Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/silver-mails-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@redocly/respect-core': patch
---

Added support for the Arazzo spec-compliant workflow reference form `$sourceDescriptions.<name>.<workflowId>`.
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('getValueFromContext', () => {
});

// $sourceDescriptions.<name>.workflows.<workflowId>
it('should return workflow from $sourceDescriptions', () => {
it('should resolve the legacy `$sourceDescriptions.<name>.workflows.<workflowId>` form for backward compatibility', () => {
const ctx = {
$sourceDescriptions: {
test: {
Expand All @@ -168,7 +168,7 @@ describe('getValueFromContext', () => {
});
});

it('should return undefined from $sourceDescriptions if there is no such sourceDescription', () => {
it('should return undefined for the legacy form when there is no such sourceDescription', () => {
const ctx = {
$sourceDescriptions: {
test: {
Expand All @@ -190,7 +190,7 @@ describe('getValueFromContext', () => {
).toEqual(undefined);
});

it('should return undefined from $sourceDescriptions if there is no workflowId provided', () => {
it('should return undefined for the legacy form when there is no workflowId provided', () => {
const ctx = {
$sourceDescriptions: {
test: {
Expand All @@ -208,6 +208,98 @@ describe('getValueFromContext', () => {
).toEqual(undefined);
});

it('should return workflow from $sourceDescriptions using the spec form without `workflows` segment', () => {
const ctx = {
$sourceDescriptions: {
test: {
workflows: [
{
workflowId: 'workflowTestId',
steps: [],
},
],
},
},
} as unknown as TestContext;
expect(
getValueFromContext({
value: '$sourceDescriptions.test.workflowTestId',
ctx,
logger,
})
).toEqual({
workflowId: 'workflowTestId',
steps: [],
});
});

it('should fall back to field access for the spec form when no workflow matches', () => {
const ctx = {
$sourceDescriptions: {
test: {
url: './test.yaml',
workflows: [
{
workflowId: 'workflowTestId',
steps: [],
},
],
},
},
} as unknown as TestContext;
expect(
getValueFromContext({
value: '$sourceDescriptions.test.url',
ctx,
logger,
})
).toEqual('./test.yaml');
});

it('should return undefined for the spec form when the source description exists but has no matching workflow or field', () => {
const ctx = {
$sourceDescriptions: {
test: {
workflows: [
{
workflowId: 'workflowTestId',
steps: [],
},
],
},
},
} as unknown as TestContext;
expect(
getValueFromContext({
value: '$sourceDescriptions.test.notExistingWorkflow',
ctx,
logger,
})
).toEqual(undefined);
});

it('should return undefined for the spec form when the source description does not exist', () => {
const ctx = {
$sourceDescriptions: {
test: {
workflows: [
{
workflowId: 'workflowTestId',
steps: [],
},
],
},
},
} as unknown as TestContext;
expect(
getValueFromContext({
value: '$sourceDescriptions.notExistingName.someWorkflow',
ctx,
logger,
})
).toEqual(undefined);
});

it('should return undefined if getFakeData had error resolving value', () => {
const ctx = {
$sourceDescriptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2628,7 +2628,7 @@ describe('runStep', () => {
it('should run step with workflowId from external workflowSpec', async () => {
const step: Step = {
stepId: 'get-bird',
workflowId: '$sourceDescriptions.reusable-api.workflows.reusable-external-workflow',
workflowId: '$sourceDescriptions.reusable-api.reusable-external-workflow',
checks: [],
response: {} as any,
};
Expand Down Expand Up @@ -2958,7 +2958,7 @@ describe('runStep', () => {
steps: [
{
stepId: 'get-bird',
workflowId: '$sourceDescriptions.reusable-api.workflows.reusable-external-workflow',
workflowId: '$sourceDescriptions.reusable-api.reusable-external-workflow',
checks: [],
},
],
Expand Down Expand Up @@ -3029,7 +3029,7 @@ describe('runStep', () => {
it('should run step with workflowId from external workflow and populate inputs from the parameters', async () => {
const step = {
stepId: 'get-bird',
workflowId: '$sourceDescriptions.reusable-api.workflows.reusable-external-workflow',
workflowId: '$sourceDescriptions.reusable-api.reusable-external-workflow',
parameters: [
{
name: 'workflowLevelParam',
Expand Down Expand Up @@ -3375,7 +3375,7 @@ describe('runStep', () => {
steps: [
{
stepId: 'get-bird',
workflowId: '$sourceDescriptions.reusable-api.workflows.reusable-external-workflow',
workflowId: '$sourceDescriptions.reusable-api.reusable-external-workflow',
parameters: [
{
name: 'workflowLevelParam',
Expand Down Expand Up @@ -3471,7 +3471,7 @@ describe('runStep', () => {
});

expect(resolveWorkflowContext).toHaveBeenCalledWith(
'$sourceDescriptions.reusable-api.workflows.reusable-external-workflow',
'$sourceDescriptions.reusable-api.reusable-external-workflow',
{
inputs: {
properties: {
Expand Down Expand Up @@ -3518,7 +3518,7 @@ describe('runStep', () => {
it('should run step with not existing workflowId and populate step.checks with an error', async () => {
const step: Step = {
stepId: 'get-bird',
workflowId: '$sourceDescriptions.wrong-reusable-api.workflows.reusable-external-workflow',
workflowId: '$sourceDescriptions.wrong-reusable-api.reusable-external-workflow',
outputs: {
stepOutput: '$outputs.reusableWorkflowOutput.stepOutput',
},
Expand Down Expand Up @@ -3928,7 +3928,7 @@ describe('runStep', () => {

expect(runWorkflow).not.toHaveBeenCalled();
expect(cleanColors(step?.checks[0]?.message || '')).toEqual(
'Workflow $sourceDescriptions.wrong-reusable-api.workflows.reusable-external-workflow not found.'
'Workflow $sourceDescriptions.wrong-reusable-api.reusable-external-workflow not found.'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ vi.mock('../../../flow-runner/context/create-test-context.js');

describe('resolveWorkflowContext', async () => {
const config = await createConfig({});
const workflowId = '$sourceDescriptions.tickets-from-museum-api.workflows.get-museum-tickets';
const workflowId = '$sourceDescriptions.tickets-from-museum-api.get-museum-tickets';
const apiClient = new ApiFetcher({});
const resolvedWorkflow = {
workflowId: 'get-museum-tickets',
Expand Down Expand Up @@ -379,6 +379,30 @@ describe('resolveWorkflowContext', async () => {
);
});

it('should still resolve the legacy `$sourceDescriptions.<name>.workflows.<workflowId>` form for backward compatibility', async () => {
const workflowId = '$sourceDescriptions.tickets-from-museum-api.workflows.get-museum-tickets';
await resolveWorkflowContext(workflowId, resolvedWorkflow, commonCtx, config);

expect(createTestContext).toHaveBeenCalledWith(
commonCtx.$sourceDescriptions['tickets-from-museum-api'],
{
input: undefined,
skip: undefined,
workflow: ['get-museum-tickets'],
filePath: expect.stringContaining('examples/museum-api/museum-tickets.yaml'),
config,
executionTimeout: 3_600_000,
maxSteps: 2000,
maxFetchTimeout: 40_000,
server: undefined,
severity: undefined,
verbose: undefined,
metadata: commonCtx.options.metadata,
},
apiClient
);
});

it('should call createTestContext with empty filePath when there are no ctx.sourceDescriptions', async () => {
const ctx = {
...commonCtx,
Expand Down Expand Up @@ -554,7 +578,7 @@ describe('resolveWorkflowContext', async () => {
},
},
} as any;
const workflowId = '$sourceDescriptions.wrong-api.workflows.get-museum-tickets';
const workflowId = '$sourceDescriptions.wrong-api.get-museum-tickets';

await expect(
resolveWorkflowContext(workflowId, resolvedWorkflow, ctx, config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,26 +316,33 @@ const resolveValue = (
return path.slice(7, -2);
}

// $sourceDescriptions.<name>.<workflowId>
// $sourceDescriptions.<name>.workflows.<workflowId>
if (path.startsWith('$sourceDescriptions.') && path.includes('.workflows.')) {
if (path.startsWith('$sourceDescriptions.')) {
const parts = path.split('.');

const sourceDescriptionName = parts[1];
const workflowId = parts[3];

if (!sourceDescriptionName || !workflowId) {
return undefined;
const isLegacyForm = parts.length === 4 && parts[2] === 'workflows';
const isSpecForm = parts.length === 3;
const workflowId = isLegacyForm ? parts[3] : isSpecForm ? parts[2] : undefined;
Comment thread
harshit078 marked this conversation as resolved.

if (sourceDescriptionName && workflowId) {
const sourceDescriptions = getFrom(ctx)('$sourceDescriptions');
const sourceDescription = sourceDescriptions?.[sourceDescriptionName];
const workflow = sourceDescription?.workflows?.find(
(workflow: Workflow) => workflow.workflowId === workflowId
);

if (workflow) {
return workflow;
}
if (isSpecForm) {
return sourceDescription?.[workflowId];
}
}

const sourceDescriptions = getFrom(ctx)('$sourceDescriptions');

if (!sourceDescriptions[sourceDescriptionName]) {
if (isLegacyForm) {
Comment thread
cursor[bot] marked this conversation as resolved.
return undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we need to handle case with

    if (isLegacyForm) {
      return undefined;
    }


return sourceDescriptions[sourceDescriptionName].workflows.find(
(workflow: Workflow) => workflow.workflowId === workflowId
);
}

if (path && path.trim().startsWith('faker.')) {
Expand Down
Loading