Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ do:
const workflow = Classes.Workflow.deserialize(text);
```

Deserialization validates the document by default. Pass `{ validate: false }` to load a work in progress definition, which is what an editor holds most of the time:

```typescript
const draft = Classes.Workflow.deserialize(text, { validate: false });
```

> [!NOTE]
> Opting out skips *validation*, not *hydration*. A definition whose shape is structurally impossible, for instance `do` written as a mapping rather than a sequence, still throws while the classes are built.

#### Create a Workflow Definition by Casting an Object

You can type-cast an object to match the structure of a workflow definition:
Expand Down Expand Up @@ -241,14 +250,43 @@ import { Classes } from '@openworkflowspec/sdk';

// const workflow = <Your preferred method>;
if (workflow instanceof Classes.Workflow) {
const yaml = workflow.serialize(/*'yaml' | 'json' */);
const yaml = workflow.serialize(/*{ format: 'yaml' | 'json' }*/);
} else {
const json = Classes.Workflow.serialize(workflow, 'json');
const json = Classes.Workflow.serialize(workflow, { format: 'json' });
}
```

Both accept a `SerializationOptions` payload, mirroring the `build({ validate, normalize })` options above:

```typescript
import { Classes } from '@openworkflowspec/sdk';
import type { SerializationOptions } from '@openworkflowspec/sdk';

const options: SerializationOptions = {
format: 'yaml', // default 'yaml'
normalize: true, // default true
validate: true, // default true
yaml: {
indent: 2, // default 2
lineWidth: 80, // default 80, -1 for unlimited
sortKeys: false, // default false
flowLevel: -1, // default -1, never switch to flow style
},
};
const text = workflow.serialize(options);
```

Set `validate: false` to save a work in progress definition, and `yaml: { lineWidth: -1 }` to stop long runtime expressions being folded into block scalars, which is usually what you want for a definition kept under version control:

```typescript
const draft = workflow.serialize({ validate: false, yaml: { lineWidth: -1 } });
```

> [!NOTE]
> The default serialization format is YAML.
> The default serialization format is YAML. The `yaml` options are ignored when the format is `json`.

> [!TIP]
> The positional form, `serialize('json')` and `serialize('yaml', false)`, still works but is deprecated in favour of the options payload.

#### Validate Workflow Definitions

Expand Down
2 changes: 1 addition & 1 deletion examples/browser/umd/using-class.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
try {
workflow.validate();
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/umd/using-fluent-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)
.build();
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/umd/using-json.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
}`;
const workflow = Workflow.deserialize(myJsonWorkflow);
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
})();
</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/umd/using-plain-object.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
try {
validate('Workflow', workflowDefinition);
document.getElementById('output').innerHTML =
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, 'json')}`;
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, { format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/using-class.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
try {
workflow.validate();
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/using-fluent-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
)
.build();
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/using-json.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
}`;
const workflow = Workflow.deserialize(myJsonWorkflow);
document.getElementById('output').innerHTML =
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`;
`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`;
})();
</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion examples/browser/using-plain-object.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
try {
validate('Workflow', workflowDefinition);
document.getElementById('output').innerHTML =
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, 'json')}`;
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, { format: 'json' })}`;
} catch (ex) {
console.error('Invalid workflow', ex);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/node/using-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const workflow = new Workflow({

try {
workflow.validate();
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`);
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`);
} catch (ex) {
console.error('Invalid workflow', ex);
}
2 changes: 1 addition & 1 deletion examples/node/using-fluent-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ try {
.build(),
)
.build();
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`);
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`);
} catch (ex) {
console.error('Invalid workflow', ex);
}
2 changes: 1 addition & 1 deletion examples/node/using-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ const myJsonWorkflow = `
]
}`;
const workflow = Classes.Workflow.deserialize(myJsonWorkflow);
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize('json')}`);
console.log(`--- YAML ---\n${workflow.serialize()}\n\n--- JSON ---\n${workflow.serialize({ format: 'json' })}`);
2 changes: 1 addition & 1 deletion examples/node/using-plain-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const workflowDefinition = {
try {
validate('Workflow', workflowDefinition);
console.log(
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, 'json')}`,
`--- YAML ---\n${Classes.Workflow.serialize(workflowDefinition)}\n\n--- JSON ---\n${Classes.Workflow.serialize(workflowDefinition, { format: 'json' })}`,
);
} catch (ex) {
console.error('Invalid workflow', ex);
Expand Down
82 changes: 71 additions & 11 deletions src/lib/generated/classes/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import { getLifecycleHooks } from '../../lifecycle-hooks';
import { validate } from '../../validation';
import { isObject } from '../../utils';
import * as yaml from 'js-yaml';
import {
DeserializationOptions,
SerializationOptions,
toSerializationOptions,
toYamlDumpOptions,
} from '../../serialization';
import { buildGraph, Graph, GraphBuildOptions } from '../../graph-builder';
import { convertToMermaidCode } from '../../mermaid-converter';

Expand Down Expand Up @@ -95,13 +101,25 @@ export class Workflow extends ObjectHydrator<Specification.Workflow> {
}

/**
* Deserializes the provided string as a Workflow
* Deserializes the provided string as a Workflow.
*
* When validation is skipped, the parsed document is still checked to be a mapping: hydration
* ignores anything else, so a scalar or a sequence would otherwise yield a blank Workflow rather
* than an error. See issue #309.
*
* @param text The YAML or JSON representation of a workflow
* @param options The deserialization options, e.g. to opt out of validation
* @returns A new Workflow instance
*/
static deserialize(text: string): WorkflowIntersection {
static deserialize(text: string, options?: DeserializationOptions): WorkflowIntersection {
const model = yaml.load(text) as Partial<Specification.Workflow>;
validate('Workflow', model);
if (options?.validate ?? true) {
validate('Workflow', model);
} else if (!isObject(model)) {
throw new Error(
`The provided text does not describe a workflow: expected a mapping, got ${Array.isArray(model) ? 'a sequence' : typeof model}`,
);
}
return new Workflow(model) as WorkflowIntersection;
}

Expand All @@ -112,19 +130,38 @@ export class Workflow extends ObjectHydrator<Specification.Workflow> {
* `asPlainObject()`: js-yaml cannot dump hydrated class instances. See issue #308.
*
* @param model The workflow to serialize
* @param options The serialization options, e.g. the format or whether to validate
* @returns A string representation of the workflow
*/
static serialize(model: Partial<WorkflowIntersection>, options?: SerializationOptions): string;

/**
* Serializes the provided workflow to YAML or JSON
* @deprecated Pass a `SerializationOptions` object instead, e.g. `serialize(workflow, { format: 'json' })`
* @param model The workflow to serialize
* @param format The format, 'yaml' or 'json', default is 'yaml'
* @param normalize If the workflow should be normalized before serialization, default true
* @returns A string representation of the workflow
*/
static serialize(model: Partial<WorkflowIntersection>, format?: 'yaml' | 'json', normalize?: boolean): string;

static serialize(
model: Partial<WorkflowIntersection>,
format: 'yaml' | 'json' = 'yaml',
normalize: boolean = true,
formatOrOptions?: 'yaml' | 'json' | SerializationOptions,
legacyNormalize?: boolean,
): string {
const options = toSerializationOptions(formatOrOptions, legacyNormalize);
const format = options.format ?? 'yaml';
const shouldNormalize = options.normalize ?? true;
const shouldValidate = options.validate ?? true;
const workflow = new Workflow(model);
workflow.validate();
const plainWorkflow = (normalize ? workflow.normalize() : workflow).asPlainObject();
return format === 'json' ? JSON.stringify(plainWorkflow) : yaml.dump(plainWorkflow);
if (shouldValidate) {
workflow.validate();
}
const plainWorkflow = (shouldNormalize ? workflow.normalize() : workflow).asPlainObject();
return format === 'json'
? JSON.stringify(plainWorkflow)
: yaml.dump(plainWorkflow, toYamlDumpOptions(options.yaml));
}

/**
Expand All @@ -148,12 +185,25 @@ export class Workflow extends ObjectHydrator<Specification.Workflow> {

/**
* Serializes the workflow to YAML or JSON
* @param options The serialization options, e.g. the format or whether to validate
* @returns A string representation of the workflow
*/
serialize(options?: SerializationOptions): string;

/**
* Serializes the workflow to YAML or JSON
* @deprecated Pass a `SerializationOptions` object instead, e.g. `serialize({ format: 'json' })`
* @param format The format, 'yaml' or 'json', default is 'yaml'
* @param normalize If the workflow should be normalized before serialization, default true
* @returns A string representation of the workflow
*/
serialize(format: 'yaml' | 'json' = 'yaml', normalize: boolean = true): string {
return Workflow.serialize(this as unknown as WorkflowIntersection, format, normalize);
serialize(format?: 'yaml' | 'json', normalize?: boolean): string;

serialize(formatOrOptions?: 'yaml' | 'json' | SerializationOptions, legacyNormalize?: boolean): string {
return Workflow.serialize(
this as unknown as WorkflowIntersection,
toSerializationOptions(formatOrOptions, legacyNormalize),
);
}

/**
Expand All @@ -178,12 +228,22 @@ export const _Workflow = Workflow as WorkflowConstructor & {
/**
* Deserializes the provided string as a Workflow
* @param text The YAML or JSON representation of a workflow
* @param options The deserialization options, e.g. to opt out of validation
* @returns A new Workflow instance
*/
deserialize(text: string): WorkflowIntersection;
deserialize(text: string, options?: DeserializationOptions): WorkflowIntersection;

/**
* Serializes the provided Workflow to YAML or JSON
* @param workflow The workflow to serialize
* @param options The serialization options, e.g. the format or whether to validate
* @returns A string representation of the workflow
*/
serialize(workflow: Partial<WorkflowIntersection>, options?: SerializationOptions): string;

/**
* Serializes the provided Workflow to YAML or JSON
* @deprecated Pass a `SerializationOptions` object instead, e.g. `serialize(workflow, { format: 'json' })`
* @param workflow The workflow to serialize
* @param format The format, 'yaml' or 'json', default is 'yaml'
* @param normalize If the workflow should be normalized before serialization, default true
Expand Down
27 changes: 17 additions & 10 deletions src/lib/hydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,26 @@ export class ArrayHydrator<T> extends Array<T> {
* Instanciates a new instance of the ArrayHydrator class.
Comment thread
JBBianchi marked this conversation as resolved.
* Copies the elements of the provided model onto the instance if it is an array.
*
* @param model - Optional array or number to initialize the instance.
* Discriminates on the model's runtime type rather than on its numeric coercion. `Number([])` is 0
* and `Number(['5'])` is 5, so an isNaN based test routed empty and single element arrays into the
* `Array(length)` constructor, hydrating `[]` as `[[]]` and `null` as `[null]`. The elements are
* assigned by index rather than spread as arguments, which would exceed the engine's argument limit
* for a large model.
*
* @param model - Optional array to copy, or a number to preallocate the instance's length.
*/
constructor(model?: Array<T> | number) {
if (!isNaN(model as number)) {
super(model as number);
if (model == null) {
super();
} else if (typeof model === 'number') {
super(model);
} else if (Array.isArray(model)) {
super(model.length);
model.forEach((item, index) => {
this[index] = item;
});
} else {
super(...((model as Array<T>) || []));
if (!model) {
model = [];
}
if (!Array.isArray(model)) {
throw new Error('The provided model should be an array');
}
throw new Error('The provided model should be an array');
}
}
}
Loading