From 38dc755a5ff0ef8c25a620fae6f85d7249d97176 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Mon, 20 Oct 2025 13:09:06 +0200 Subject: [PATCH 01/19] chore: add support for entity files node types and validation --- packages/cli/src/__tests__/fixtures/config.ts | 3 + packages/cli/src/__tests__/utils.test.ts | 2 + packages/core/src/__tests__/consts.ts | 356 +++++++++++++++++ .../core/src/__tests__/entity-yaml.test.ts | 75 ++++ packages/core/src/__tests__/lint.test.ts | 366 ++++++++++++++++++ packages/core/src/bundle/bundle-visitor.ts | 2 + .../__snapshots__/config.test.ts.snap | 3 + .../__tests__/__snapshots__/load.test.ts.snap | 3 + .../core/src/config/__tests__/config.test.ts | 3 + packages/core/src/config/config.ts | 7 + packages/core/src/detect-spec.ts | 3 + packages/core/src/index.ts | 11 +- packages/core/src/lint.ts | 85 ++++ packages/core/src/oas-types.ts | 13 +- .../rules/catalogEntity1/entity-key-valid.ts | 46 +++ .../common/__tests__/entity-key-valid.test.ts | 154 ++++++++ packages/core/src/types/entity-yaml.ts | 35 ++ packages/core/src/visitors.ts | 1 + 18 files changed, 1166 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/__tests__/consts.ts create mode 100644 packages/core/src/__tests__/entity-yaml.test.ts create mode 100644 packages/core/src/rules/catalogEntity1/entity-key-valid.ts create mode 100644 packages/core/src/rules/common/__tests__/entity-key-valid.test.ts create mode 100644 packages/core/src/types/entity-yaml.ts diff --git a/packages/cli/src/__tests__/fixtures/config.ts b/packages/cli/src/__tests__/fixtures/config.ts index 24ddac4e23..f0f5a2724f 100644 --- a/packages/cli/src/__tests__/fixtures/config.ts +++ b/packages/cli/src/__tests__/fixtures/config.ts @@ -25,6 +25,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + catalogEntity1: {}, }, preprocessors: { oas2: {}, @@ -35,6 +36,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + catalogEntity1: {}, }, plugins: [], doNotResolveExamples: false, @@ -47,6 +49,7 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, + catalogEntity1: {}, }, resolveIgnore: vi.fn(), addProblemToIgnore: vi.fn(), diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts index 58e6ea0d4a..fa0006c691 100644 --- a/packages/cli/src/__tests__/utils.test.ts +++ b/packages/cli/src/__tests__/utils.test.ts @@ -546,6 +546,7 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, + catalogEntity1: {}, }; expect(() => checkIfRulesetExist(rules)).toThrowError( '⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/' @@ -562,6 +563,7 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, + catalogEntity1: {}, }; checkIfRulesetExist(rules); }); diff --git a/packages/core/src/__tests__/consts.ts b/packages/core/src/__tests__/consts.ts new file mode 100644 index 0000000000..f3a5c5e43b --- /dev/null +++ b/packages/core/src/__tests__/consts.ts @@ -0,0 +1,356 @@ +export const ENTITY_RELATION_TYPES = [ + 'partOf', + 'hasParts', + 'creates', + 'createdBy', + 'owns', + 'ownedBy', + 'implements', + 'implementedBy', + 'dependsOn', + 'dependencyOf', + 'uses', + 'usedBy', + 'produces', + 'consumes', + 'linksTo', + 'supersedes', + 'supersededBy', + 'compatibleWith', + 'extends', + 'extendedBy', + 'relatesTo', + 'hasMember', + 'memberOf', + 'triggers', + 'triggeredBy', + 'returns', + 'returnedBy', +] as const; + +export const userMetadataSchema = { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Email of the user', + }, + }, + required: ['email'], + additionalProperties: true, +} as const; + +export const apiDescriptionMetadataSchema = { + type: 'object', + properties: { + specType: { + type: 'string', + enum: ['jsonschema', 'openapi', 'asyncapi', 'avro', 'zod', 'graphql', 'protobuf', 'arazzo'], + description: 'Type of the API description', + }, + descriptionFile: { + type: 'string', + description: 'Path to the file containing the API description', + }, + }, + required: ['specType', 'descriptionFile'], + additionalProperties: true, +} as const; + +export const apiOperationMetadataSchema = { + type: 'object', + properties: { + method: { + type: 'string', + enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'MUTATION', 'QUERY', 'SUBSCRIBE', 'PUBLISH'], + description: 'HTTP method of the API operation', + }, + path: { + type: 'string', + description: 'Path of the API operation', + }, + payload: { + type: 'array', + items: { + type: 'string', + description: 'Related dataSchema name', + }, + }, + responses: { + type: 'array', + items: { + type: 'string', + description: 'Related dataSchema name', + }, + }, + }, + required: ['method', 'path'], + additionalProperties: true, +} as const; + +export const dataSchemaMetadataSchema = { + type: 'object', + properties: { + specType: { + type: 'string', + enum: ['jsonschema', 'openapi', 'asyncapi', 'avro', 'zod', 'graphql', 'protobuf', 'arazzo'], + description: 'Specification type of the data schema', + }, + schema: { + type: 'string', + description: 'Inline schema of the data structure', + }, + sdl: { + type: 'string', + description: 'SDL of the data structure', + }, + }, + required: ['specType'], + // oneOf: [{ required: ['schema'] }, { required: ['sdl'] }], + additionalProperties: true, +} as const; + +export const defaultMetadataSchema = { + type: 'object', + additionalProperties: true, +} as const; + +export const slackChannelFileSchema = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 2, + maxLength: 150, + }, + }, + required: ['name'], + additionalProperties: false, +} as const; + +export const slackContactFileSchema = { + type: 'object', + properties: { + channels: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 2, + maxLength: 150, + }, + }, + required: ['name'], + additionalProperties: false, + }, + }, + }, + required: ['channels'], + additionalProperties: false, +} as const; + +export const entityContactFileSchema = { + type: 'object', + properties: { + slack: { + type: 'object', + properties: { + channels: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 2, + maxLength: 150, + }, + url: { + type: 'string', + }, + }, + required: ['name'], + additionalProperties: false, + }, + }, + }, + required: ['channels'], + additionalProperties: false, + }, + }, + additionalProperties: false, +} as const; + +export const entityLinkFileSchema = { + type: 'object', + properties: { + label: { + type: 'string', + minLength: 2, + maxLength: 150, + }, + url: { + type: 'string', + }, + }, + required: ['label', 'url'], + additionalProperties: false, +} as const; + +export const entityRelationFileSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ENTITY_RELATION_TYPES, + }, + key: { + type: 'string', + minLength: 2, + maxLength: 100, + }, + }, + required: ['type', 'key'], + additionalProperties: false, +} as const; + +// Base entity schema properties +export const entityBaseProperties = { + key: { + type: 'string', + pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', + minLength: 2, + maxLength: 150, + }, + type: { + type: 'string', + enum: ['user', 'data-schema', 'api-operation', 'api-description', 'service', 'domain', 'team'], + }, + title: { + type: 'string', + minLength: 2, + maxLength: 200, + }, + summary: { + type: 'string', + minLength: 1, + maxLength: 500, + }, + tags: { + type: 'array', + items: { + type: 'string', + minLength: 1, + maxLength: 50, + }, + }, + git: { + type: 'array', + items: { + type: 'string', + }, + }, + contact: { + type: 'object', + additionalProperties: true, + }, + links: { + type: 'array', + items: entityLinkFileSchema, + }, + relations: { + type: 'array', + items: entityRelationFileSchema, + }, + metadata: { + type: 'object', + additionalProperties: true, + }, +} as const; + +export const entityFileSchema = { + type: 'object', + discriminator: { + propertyName: 'type', + }, + oneOf: [ + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'user' }, + metadata: userMetadataSchema, + }, + required: ['key', 'title', 'type', 'metadata'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'api-operation' }, + metadata: apiOperationMetadataSchema, + }, + required: ['key', 'title', 'type', 'metadata'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'data-schema' }, + metadata: dataSchemaMetadataSchema, + }, + required: ['key', 'title', 'type', 'metadata'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'api-description' }, + metadata: apiDescriptionMetadataSchema, + }, + required: ['key', 'title', 'type', 'metadata'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'service' }, + }, + required: ['key', 'title', 'type'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'domain' }, + }, + required: ['key', 'title', 'type'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + ...entityBaseProperties, + type: { const: 'team' }, + }, + required: ['key', 'title', 'type'], + additionalProperties: false, + }, + ], +} as const; + +export const entityFileDefaultSchema = { + type: 'object', + properties: { + ...entityBaseProperties, + }, + required: ['key', 'title', 'type'], + additionalProperties: false, +} as const; diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts new file mode 100644 index 0000000000..ad9fee59b9 --- /dev/null +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { createEntityTypes } from '../types/entity-yaml.js'; +import { normalizeTypes } from '../types/index.js'; +import { entityFileSchema, entityFileDefaultSchema } from './consts.js'; + +describe('entity-yaml', () => { + it('should create entity types with discriminator', () => { + const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); + + expect(entityTypes).toHaveProperty('user'); + expect(entityTypes).toHaveProperty('api-operation'); + expect(entityTypes).toHaveProperty('data-schema'); + expect(entityTypes).toHaveProperty('api-description'); + expect(entityTypes).toHaveProperty('service'); + expect(entityTypes).toHaveProperty('domain'); + expect(entityTypes).toHaveProperty('team'); + expect(entityTypes).toHaveProperty('EntityFileDefault'); + expect(entityTypes).toHaveProperty('EntityFileArray'); + }); + + it('should resolve entity type based on discriminator for array items', () => { + const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); + const normalizedTypes = normalizeTypes(entityTypes); + + const entityFileArrayNode = normalizedTypes['EntityFileArray']; + if (typeof entityFileArrayNode.items === 'function') { + const userEntity = { + type: 'user', + key: 'john-doe', + title: 'John Doe', + metadata: { email: 'john@example.com' }, + }; + const resolvedType = entityFileArrayNode.items(userEntity, 'root'); + + expect(resolvedType).toBeTruthy(); + if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { + expect(resolvedType.name).toBe('user'); + if ('properties' in resolvedType) { + expect(resolvedType.properties).toHaveProperty('metadata'); + } + } + } + + // Test default resolution when no type + if (typeof entityFileArrayNode.items === 'function') { + const unknownEntity = { key: 'unknown', title: 'Unknown' }; + const resolvedType = entityFileArrayNode.items(unknownEntity, 'root'); + console.log('Resolved type for entity without type:', resolvedType); + expect(resolvedType).toBeTruthy(); + if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { + expect(resolvedType.name).toBe('EntityFileDefault'); + } + } + }); + + it('should have correct required fields for entity types', () => { + const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); + const normalizedTypes = normalizeTypes(entityTypes); + + // Test that user type has correct required fields + const userNode = normalizedTypes['user']; + if (userNode && Array.isArray(userNode.required)) { + expect(userNode.required).toContain('key'); + expect(userNode.required).toContain('title'); + expect(userNode.required).toContain('type'); + } + + const defaultNode = normalizedTypes['EntityFileDefault']; + if (defaultNode && Array.isArray(defaultNode.required)) { + expect(defaultNode.required).toContain('type'); + expect(defaultNode.required).toContain('key'); + expect(defaultNode.required).toContain('title'); + } + }); +}); diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index b7e5099bef..884d515269 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -8,6 +8,11 @@ import { detectSpec } from '../detect-spec.js'; import { rootRedoclyConfigSchema } from '@redocly/config'; import { createConfigTypes } from '../types/redocly-yaml.js'; import { fileURLToPath } from 'node:url'; +import { describe, it, expect } from 'vitest'; +import { lintEntityFile } from '../lint.js'; +import { entityFileSchema, entityFileDefaultSchema } from './consts.js'; +import { makeDocumentFromString } from '../resolve.js'; +import { type Config } from '../config/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -1959,4 +1964,365 @@ describe('lint', () => { expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); + + describe('lintEntityFile', () => { + it('should lint a valid user entity file', async () => { + const entityYaml = ` + type: user + key: john-doe + title: John Doe + summary: Senior Software Engineer + metadata: + email: john@example.com + tags: + - engineering + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBe(0); + }); + + it('should detect missing required fields in entity', async () => { + const entityYaml = ` + type: user + key: john-doe + # Missing required 'title' field + metadata: + email: john@example.com + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect(problems.some((p) => p.message.includes('title'))).toBe(true); + }); + + it('should lint array of entities', async () => { + const entitiesYaml = ` + - type: user + key: john-doe + title: John Doe + metadata: + email: john@example.com + + - type: service + key: api-service + title: API Service + summary: Core API service + + - type: api-description + key: users-api + title: Users API + metadata: + specType: openapi + descriptionFile: users-api.yaml + `; + + const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBe(0); + }); + + it('should use default schema when type is missing', async () => { + const entityYaml = ` + key: unknown-entity + title: Unknown Entity + summary: An entity without a type field + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect(problems.some((p) => p.message.includes('type'))).toBe(true); + }); + + it('should detect missing metadata.email for user entity', async () => { + const entityYaml = ` + type: user + key: john-doe + title: John Doe + metadata: + name: John + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect(problems.some((p) => p.message.includes('email'))).toBe(true); + }); + + it('should detect missing metadata for user entity', async () => { + const entityYaml = ` + type: user + key: john-doe + title: John Doe + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect(problems.some((p) => p.message.includes('metadata'))).toBe(true); + }); + + it('should not validate patterns with Struct rule', async () => { + const entityYaml = ` + type: service + key: Invalid_Key_With_Underscores + title: Invalid Service + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBe(1); + expect(problems[0].ruleId).toBe('entity key-valid'); + expect(problems[0].message).toContain('lowercase letters'); + }); + + it('should detect missing metadata fields for specific entity types', async () => { + const entityYaml = ` + type: api-description + key: my-api + title: My API + metadata: + specType: openapi + # Missing descriptionFile + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + }); + + it('should detect invalid entity type in array', async () => { + const entitiesYaml = ` + - type: user + key: john-doe + title: John Doe + metadata: + email: john@example.com + + - type: service + key: invalid-service + # Missing required title field + `; + + const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect(problems.some((p) => p.message.includes('title'))).toBe(true); + }); + + it('should validate service entity without metadata', async () => { + const entityYaml = ` + type: service + key: my-service + title: My Service + summary: A simple service + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBe(0); + }); + + it('should detect invalid relation type', async () => { + const entityYaml = ` + type: service + key: my-service + title: My Service + relations: + - type: invalidRelationType + key: some-entity + `; + + const document = makeDocumentFromString(entityYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + expect( + problems.some( + (p) => + p.message.toLowerCase().includes('enum') || + p.message.includes('invalidRelationType') || + p.message.includes('type') + ) + ).toBe(true); + }); + + it('should validate type-specific metadata fields correctly', async () => { + const apiOperationYaml = ` + type: api-operation + key: test-operation + title: Test Operation + metadata: + wrongField: value + `; + + const document = makeDocumentFromString(apiOperationYaml, '/entity.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + const hasMethodOrPathError = problems.some( + (p) => p.message.includes('method') || p.message.includes('path') + ); + const hasEmailError = problems.some((p) => p.message.includes('email')); + + expect(hasEmailError).toBe(false); + expect(hasMethodOrPathError).toBe(true); + }); + + it('should validate different metadata schemas in array of mixed entity types', async () => { + const mixedEntitiesYaml = ` + - type: api-description + key: my-api + title: My API + metadata: + specType: openapi + # Missing descriptionFile + + - type: api-operation + key: my-operation + title: My Operation + metadata: + wrongField: value + # Missing method and path + + - type: data-schema + key: my-schema + title: My Schema + metadata: + wrongField: value + # Missing specType + `; + + const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + + const hasDescriptionFileError = problems.some((p) => p.message.includes('descriptionFile')); + + const hasMethodError = problems.some((p) => p.message.includes('method')); + const hasPathError = problems.some((p) => p.message.includes('path')); + + const hasSpecTypeError = problems.some((p) => p.message.includes('specType')); + + const hasEmailError = problems.some((p) => p.message.includes('email')); + + expect(hasDescriptionFileError).toBe(true); + expect(hasMethodError).toBe(true); + expect(hasPathError).toBe(true); + expect(hasSpecTypeError).toBe(true); + + expect(hasEmailError).toBe(false); + }); + + it('should handle entity without type in array using default schema', async () => { + const mixedEntitiesYaml = ` + - type: user + key: valid-user + title: Valid User + metadata: + email: user@example.com + + - key: no-type-entity + title: Entity Without Type + summary: This entity is missing the type field + # Missing type - should use EntityFileDefault schema + + - type: service + key: valid-service + title: Valid Service + `; + + const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); + + const problems = await lintEntityFile({ + config: { document } as Config, + entitySchema: entityFileSchema, + entityDefaultSchema: entityFileDefaultSchema, + }); + + expect(problems.length).toBeGreaterThan(0); + const hasTypeError = problems.some((p) => p.message.includes('type')); + + expect(hasTypeError).toBe(true); + }); + }); }); diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index bcd16978d4..a17a11ceb2 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -79,6 +79,8 @@ export function mapTypeToComponent(typeName: string, version: SpecMajorVersion) default: return null; } + case 'catalogEntity1': + return null; } } diff --git a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap index 5957427ec5..e71b1c1230 100644 --- a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap @@ -10,6 +10,7 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -35,6 +36,7 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -62,6 +64,7 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, + "catalogEntity1": {}, "oas2": { "no-empty-servers": "error", "operation-summary": "error", diff --git a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap index 573e01e946..0ead39e1be 100644 --- a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap @@ -10,6 +10,7 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -24,6 +25,7 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -153,6 +155,7 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, + "catalogEntity1": {}, "oas2": { "assertions": [ { diff --git a/packages/core/src/config/__tests__/config.test.ts b/packages/core/src/config/__tests__/config.test.ts index c3ada9d8a3..e6a174ef73 100644 --- a/packages/core/src/config/__tests__/config.test.ts +++ b/packages/core/src/config/__tests__/config.test.ts @@ -52,6 +52,7 @@ describe('Config.forAlias', () => { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -92,6 +93,7 @@ describe('Config.forAlias', () => { "arazzo1": {}, "async2": {}, "async3": {}, + "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -157,6 +159,7 @@ describe('Config.forAlias', () => { "no-empty-servers": "error", "operation-summary": "warn", }, + "catalogEntity1": {}, "oas2": { "no-empty-servers": "error", "operation-summary": "warn", diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index a93a238ce2..984c2c9052 100755 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -95,6 +95,7 @@ export class Config { async3: group({ ...resolvedConfig.rules, ...resolvedConfig.async3Rules }), arazzo1: group({ ...resolvedConfig.rules, ...resolvedConfig.arazzo1Rules }), overlay1: group({ ...resolvedConfig.rules, ...resolvedConfig.overlay1Rules }), + catalogEntity1: group({}), }; this.preprocessors = { @@ -127,6 +128,7 @@ export class Config { ...resolvedConfig.preprocessors, ...resolvedConfig.overlay1Preprocessors, }, + catalogEntity1: {}, }; this.decorators = { @@ -141,6 +143,7 @@ export class Config { ...resolvedConfig.decorators, ...resolvedConfig.overlay1Decorators, }, + catalogEntity1: {}, }; this.resolveIgnore(getIgnoreFilePath(opts.configPath)); @@ -264,6 +267,8 @@ export class Config { if (!plugin.typeExtension.overlay1) continue; extendedTypes = plugin.typeExtension.overlay1(extendedTypes, version); break; + case 'catalogEntity1': + continue; default: throw new Error('Not implemented'); } @@ -397,6 +402,8 @@ export class Config { (p) => p.decorators?.overlay1 && overlay1Rules.push(p.decorators.overlay1) ); return overlay1Rules; + case 'catalogEntity1': + return []; } } diff --git a/packages/core/src/detect-spec.ts b/packages/core/src/detect-spec.ts index f63b989f1a..5b2279fe5a 100644 --- a/packages/core/src/detect-spec.ts +++ b/packages/core/src/detect-spec.ts @@ -12,6 +12,7 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', + 'catalogEntity1', ] as const; export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { @@ -25,6 +26,8 @@ export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { return 'arazzo1'; } else if (version === 'overlay1') { return 'overlay1'; + } else if (version === 'catalogEntity1') { + return 'catalogEntity1'; } else { return 'oas3'; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4ef00aea68..6b5099428f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,6 +20,7 @@ export { AsyncApi3Types } from './types/asyncapi3.js'; export { Arazzo1Types } from './types/arazzo.js'; export { Overlay1Types } from './types/overlay.js'; export { ConfigTypes, createConfigTypes } from './types/redocly-yaml.js'; +export { createEntityTypes } from './types/entity-yaml.js'; export { normalizeTypes, type NormalizedNodeType, type NodeType } from './types/index.js'; export { Stats } from './rules/other/stats.js'; export { @@ -98,7 +99,14 @@ export { } from './walk.js'; export { getAstNodeByPointer, getLineColLocation, getCodeframe } from './format/codeframes.js'; export { formatProblems, getTotals, type OutputFormat, type Totals } from './format/format.js'; -export { lint, lint as validate, lintDocument, lintFromString, lintConfig } from './lint.js'; +export { + lint, + lint as validate, + lintDocument, + lintFromString, + lintConfig, + lintEntityFile, +} from './lint.js'; export { bundle, bundleFromString, type BundleResult } from './bundle/bundle.js'; export { bundleDocument } from './bundle/bundle-document.js'; export { mapTypeToComponent } from './bundle/bundle-visitor.js'; @@ -148,3 +156,4 @@ export type { ResolvedSecurity, } from './typings/arazzo.js'; export type { StatsAccumulator, StatsName } from './typings/common.js'; +export { getNodeTypesFromJSONSchema } from './types/json-schema-adapter.js'; diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index a3a295f20a..ab96e4389f 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -8,9 +8,12 @@ import { releaseAjvInstance } from './rules/ajv.js'; import { getTypes } from './oas-types.js'; import { detectSpec, getMajorSpecVersion } from './detect-spec.js'; import { createConfigTypes } from './types/redocly-yaml.js'; +import { createEntityTypes } from './types/entity-yaml.js'; import { Struct } from './rules/common/struct.js'; import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js'; +import { EntityKeyValid } from './rules/catalogEntity1/entity-key-valid.js'; import { type Config } from './config/index.js'; +import { isPlainObject } from './utils/is-plain-object.js'; import type { Document } from './resolve.js'; import type { ProblemSeverity, WalkContext } from './walk.js'; @@ -19,6 +22,7 @@ import type { Arazzo1Visitor, Async2Visitor, Async3Visitor, + BaseVisitor, NestedVisitObject, Oas2Visitor, Oas3Visitor, @@ -26,6 +30,7 @@ import type { RuleInstanceConfig, } from './visitors.js'; import type { CollectFn } from './utils/types.js'; +import type { JSONSchema } from 'json-schema-to-ts'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore FIXME: remove this once we remove `theme` from the schema @@ -193,3 +198,83 @@ export async function lintConfig(opts: { return ctx.problems; } + +export async function lintEntityFile(opts: { + config: Config; + entitySchema: JSONSchema; + entityDefaultSchema: JSONSchema; + severity?: ProblemSeverity; + externalRefResolver?: BaseResolver; +}) { + const { + config, + entitySchema, + entityDefaultSchema, + severity, + externalRefResolver = new BaseResolver(), + } = opts; + + if (!config.document) { + throw new Error('Config document is not set.'); + } + + const ctx: WalkContext = { + problems: [], + specVersion: 'catalogEntity1', + config, + visitorsData: {}, + }; + + const entityTypes = createEntityTypes(entitySchema, entityDefaultSchema); + const types = normalizeTypes(entityTypes); + + let rootType = types.EntityFileDefault; + if (Array.isArray(config.document.parsed)) { + rootType = types.EntityFileArray; + } else if (isPlainObject(config.document.parsed)) { + const ENTITY_DISCRIMINATOR_NAME = 'type'; + const typeValue = (config.document.parsed as Record)[ + ENTITY_DISCRIMINATOR_NAME + ]; + if (typeof typeValue === 'string' && types[typeValue]) { + rootType = types[typeValue]; + } + } + + const rules: (RuleInstanceConfig & { + visitor: NestedVisitObject; + })[] = [ + { + severity: severity || 'error', + ruleId: 'entity struct', + visitor: Struct({ severity: 'error' }), + }, + { + severity: severity || 'error', + ruleId: 'entity no-unresolved-refs', + visitor: NoUnresolvedRefs({ severity: 'error' }), + }, + { + severity: severity || 'error', + ruleId: 'entity key-valid', + visitor: EntityKeyValid({ severity: 'error' }), + }, + ]; + + const normalizedVisitors = normalizeVisitors(rules, types); + const resolvedRefMap = await resolveDocument({ + rootDocument: config.document, + rootType, + externalRefResolver, + }); + + walkDocument({ + document: config.document, + rootType, + normalizedVisitors, + resolvedRefMap, + ctx, + }); + + return ctx.problems; +} diff --git a/packages/core/src/oas-types.ts b/packages/core/src/oas-types.ts index 8ffc94ca9b..f2e1597b28 100644 --- a/packages/core/src/oas-types.ts +++ b/packages/core/src/oas-types.ts @@ -40,10 +40,18 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', + 'catalogEntity1', ] as const; export type SpecVersion = typeof specVersions[number]; -export type SpecMajorVersion = 'oas2' | 'oas3' | 'async2' | 'async3' | 'arazzo1' | 'overlay1'; +export type SpecMajorVersion = + | 'oas2' + | 'oas3' + | 'async2' + | 'async3' + | 'arazzo1' + | 'overlay1' + | 'catalogEntity1'; const typesMap = { oas2: Oas2Types, @@ -106,5 +114,8 @@ export type Arazzo1DecoratorsSet = Record; export type Overlay1DecoratorsSet = Record; export function getTypes(spec: SpecVersion) { + if (spec === 'catalogEntity1') { + return {}; + } return typesMap[spec]; } diff --git a/packages/core/src/rules/catalogEntity1/entity-key-valid.ts b/packages/core/src/rules/catalogEntity1/entity-key-valid.ts new file mode 100644 index 0000000000..de056d2831 --- /dev/null +++ b/packages/core/src/rules/catalogEntity1/entity-key-valid.ts @@ -0,0 +1,46 @@ +import type { UserContext } from '../../walk.js'; +import type { CatalogEntity1Rule } from '../../visitors.js'; + +const validKeyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const MIN_KEY_LENGTH = 2; +const MAX_KEY_LENGTH = 150; + +export const EntityKeyValid: CatalogEntity1Rule = () => { + return { + any(node: any, { report, location }: UserContext) { + if (typeof node === 'object' && node !== null && 'key' in node) { + const key = node.key; + + if (typeof key !== 'string') { + report({ + message: 'Entity `key` must be a string.', + location: location.child(['key']), + }); + return; + } + + if (key.length < MIN_KEY_LENGTH) { + report({ + message: `Entity \`key\` must be at least ${MIN_KEY_LENGTH} characters long.`, + location: location.child(['key']), + }); + } + + if (key.length > MAX_KEY_LENGTH) { + report({ + message: `Entity \`key\` must not exceed ${MAX_KEY_LENGTH} characters.`, + location: location.child(['key']), + }); + } + + if (!validKeyPattern.test(key)) { + report({ + message: + 'Entity `key` must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen.', + location: location.child(['key']), + }); + } + } + }, + }; +}; diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts new file mode 100644 index 0000000000..161a7a9362 --- /dev/null +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from 'vitest'; +import { makeDocumentFromString } from '../../../resolve.js'; +import { Config } from '../../../config/index.js'; +import { createEntityTypes } from '../../../types/entity-yaml.js'; +import { normalizeTypes } from '../../../types/index.js'; +import { normalizeVisitors } from '../../../visitors.js'; +import { walkDocument } from '../../../walk.js'; +import { EntityKeyValid } from '../../catalogEntity1/entity-key-valid.js'; +import type { WalkContext } from '../../../walk.js'; + +const entityFileSchema = { + type: 'object', + properties: { + type: { type: 'string' }, + key: { + type: 'string', + pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', + minLength: 2, + maxLength: 150, + }, + title: { type: 'string' }, + }, + required: ['type', 'key', 'title'], +} as const; + +const entityFileDefaultSchema = { + type: 'object', + properties: { + type: { type: 'string' }, + key: { + type: 'string', + pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', + minLength: 2, + maxLength: 150, + }, + title: { type: 'string' }, + }, + required: ['type', 'key', 'title'], +} as const; + +function lintEntityKey(source: string): WalkContext['problems'] { + const document = makeDocumentFromString(source, '/test.yaml'); + const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); + const types = normalizeTypes(entityTypes); + + const ctx: WalkContext = { + problems: [], + specVersion: 'oas3_0', + config: {} as Config, + visitorsData: {}, + }; + + const rules = [ + { + severity: 'error' as const, + ruleId: 'entity key-valid', + visitor: EntityKeyValid({ severity: 'error' }) as any, + }, + ]; + + const normalizedVisitors = normalizeVisitors(rules as any, types); + + walkDocument({ + document, + rootType: types.EntityFileDefault, + normalizedVisitors, + resolvedRefMap: new Map(), + ctx, + }); + + return ctx.problems; +} + +describe('EntityKeyValid rule', () => { + it('should pass for valid entity key', () => { + const problems = lintEntityKey(` +type: user +key: valid-key-123 +title: Valid User +`); + expect(problems).toHaveLength(0); + }); + + it('should fail for key that is too short', () => { + const problems = lintEntityKey(` +type: user +key: a +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('at least 2 characters'); + }); + + it('should fail for key with uppercase letters', () => { + const problems = lintEntityKey(` +type: user +key: Invalid-Key +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('lowercase letters'); + }); + + it('should fail for key starting with hyphen', () => { + const problems = lintEntityKey(` +type: user +key: -invalid-key +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('cannot start or end with a hyphen'); + }); + + it('should fail for key ending with hyphen', () => { + const problems = lintEntityKey(` +type: user +key: invalid-key- +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('cannot start or end with a hyphen'); + }); + + it('should fail for key with invalid characters', () => { + const problems = lintEntityKey(` +type: user +key: invalid_key@ +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('lowercase letters'); + }); + + it('should fail for key that exceeds maximum length', () => { + const longKey = 'a'.repeat(151); + const problems = lintEntityKey(` +type: user +key: ${longKey} +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('must not exceed 150 characters'); + }); + + it('should fail for non-string key', () => { + const problems = lintEntityKey(` +type: user +key: 123 +title: Valid User +`); + expect(problems).toHaveLength(1); + expect(problems[0].message).toContain('must be a string'); + }); +}); diff --git a/packages/core/src/types/entity-yaml.ts b/packages/core/src/types/entity-yaml.ts new file mode 100644 index 0000000000..5236be7286 --- /dev/null +++ b/packages/core/src/types/entity-yaml.ts @@ -0,0 +1,35 @@ +import { getNodeTypesFromJSONSchema } from './json-schema-adapter.js'; +import { isPlainObject } from '../utils/is-plain-object.js'; + +import type { JSONSchema } from 'json-schema-to-ts'; +import type { NodeType } from './index.js'; + +export function createEntityTypes( + entitySchema: JSONSchema, + entityDefaultSchema: JSONSchema +): Record { + const ENTITY_DISCRIMINATOR_NAME = 'type'; + + const defaultNodeTypes = getNodeTypesFromJSONSchema('EntityFileDefault', entityDefaultSchema); + + const namedNodeTypes = getNodeTypesFromJSONSchema('EntityFile', entitySchema); + + const arrayNodeType = { + properties: {}, + items: (value: unknown) => { + if (isPlainObject(value)) { + const typeValue = value[ENTITY_DISCRIMINATOR_NAME]; + if (typeof typeValue === 'string' && namedNodeTypes[typeValue]) { + return typeValue; + } + } + return 'EntityFileDefault'; + }, + }; + + return { + ...defaultNodeTypes, + ...namedNodeTypes, + EntityFileArray: arrayNodeType, + }; +} diff --git a/packages/core/src/visitors.ts b/packages/core/src/visitors.ts index 1958872f3c..a6632ee268 100644 --- a/packages/core/src/visitors.ts +++ b/packages/core/src/visitors.ts @@ -377,6 +377,7 @@ export type Async2Rule = (options: Record) => Async2Visitor | Async export type Async3Rule = (options: Record) => Async3Visitor | Async3Visitor[]; export type Arazzo1Rule = (options: Record) => Arazzo1Visitor | Arazzo1Visitor[]; export type Overlay1Rule = (options: Record) => Overlay1Visitor | Overlay1Visitor[]; +export type CatalogEntity1Rule = (options: Record) => BaseVisitor | BaseVisitor[]; export type Oas3Preprocessor = (options: Record) => Oas3Visitor; export type Oas2Preprocessor = (options: Record) => Oas2Visitor; export type Async2Preprocessor = (options: Record) => Async2Visitor; From 85ce02d4f5acd25dc73136159f2e3aa3fbb21e48 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 22 Oct 2025 13:04:02 +0200 Subject: [PATCH 02/19] chore: remove catalogEntity1 format --- packages/cli/src/__tests__/fixtures/config.ts | 3 --- packages/cli/src/__tests__/utils.test.ts | 2 -- packages/core/src/bundle/bundle-visitor.ts | 2 -- .../__tests__/__snapshots__/config.test.ts.snap | 3 --- .../__tests__/__snapshots__/load.test.ts.snap | 3 --- packages/core/src/config/__tests__/config.test.ts | 3 --- packages/core/src/config/config.ts | 7 ------- packages/core/src/detect-spec.ts | 3 --- packages/core/src/lint.ts | 4 ++-- packages/core/src/oas-types.ts | 13 +------------ .../rules/common/__tests__/entity-key-valid.test.ts | 2 +- .../{catalogEntity1 => common}/entity-key-valid.ts | 4 ++-- packages/core/src/visitors.ts | 1 - 13 files changed, 6 insertions(+), 44 deletions(-) rename packages/core/src/rules/{catalogEntity1 => common}/entity-key-valid.ts (91%) diff --git a/packages/cli/src/__tests__/fixtures/config.ts b/packages/cli/src/__tests__/fixtures/config.ts index f0f5a2724f..24ddac4e23 100644 --- a/packages/cli/src/__tests__/fixtures/config.ts +++ b/packages/cli/src/__tests__/fixtures/config.ts @@ -25,7 +25,6 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, - catalogEntity1: {}, }, preprocessors: { oas2: {}, @@ -36,7 +35,6 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, - catalogEntity1: {}, }, plugins: [], doNotResolveExamples: false, @@ -49,7 +47,6 @@ export const configFixture: Config = { async3: {}, arazzo1: {}, overlay1: {}, - catalogEntity1: {}, }, resolveIgnore: vi.fn(), addProblemToIgnore: vi.fn(), diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts index fa0006c691..58e6ea0d4a 100644 --- a/packages/cli/src/__tests__/utils.test.ts +++ b/packages/cli/src/__tests__/utils.test.ts @@ -546,7 +546,6 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, - catalogEntity1: {}, }; expect(() => checkIfRulesetExist(rules)).toThrowError( '⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/' @@ -563,7 +562,6 @@ describe('checkIfRulesetExist', () => { async3: {}, arazzo1: {}, overlay1: {}, - catalogEntity1: {}, }; checkIfRulesetExist(rules); }); diff --git a/packages/core/src/bundle/bundle-visitor.ts b/packages/core/src/bundle/bundle-visitor.ts index a17a11ceb2..bcd16978d4 100644 --- a/packages/core/src/bundle/bundle-visitor.ts +++ b/packages/core/src/bundle/bundle-visitor.ts @@ -79,8 +79,6 @@ export function mapTypeToComponent(typeName: string, version: SpecMajorVersion) default: return null; } - case 'catalogEntity1': - return null; } } diff --git a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap index e71b1c1230..5957427ec5 100644 --- a/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/config.test.ts.snap @@ -10,7 +10,6 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -36,7 +35,6 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -64,7 +62,6 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, - "catalogEntity1": {}, "oas2": { "no-empty-servers": "error", "operation-summary": "error", diff --git a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap index 0ead39e1be..573e01e946 100644 --- a/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/load.test.ts.snap @@ -10,7 +10,6 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -25,7 +24,6 @@ Config { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -155,7 +153,6 @@ Config { "no-empty-servers": "error", "operation-summary": "error", }, - "catalogEntity1": {}, "oas2": { "assertions": [ { diff --git a/packages/core/src/config/__tests__/config.test.ts b/packages/core/src/config/__tests__/config.test.ts index e6a174ef73..c3ada9d8a3 100644 --- a/packages/core/src/config/__tests__/config.test.ts +++ b/packages/core/src/config/__tests__/config.test.ts @@ -52,7 +52,6 @@ describe('Config.forAlias', () => { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -93,7 +92,6 @@ describe('Config.forAlias', () => { "arazzo1": {}, "async2": {}, "async3": {}, - "catalogEntity1": {}, "oas2": {}, "oas3_0": {}, "oas3_1": {}, @@ -159,7 +157,6 @@ describe('Config.forAlias', () => { "no-empty-servers": "error", "operation-summary": "warn", }, - "catalogEntity1": {}, "oas2": { "no-empty-servers": "error", "operation-summary": "warn", diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 984c2c9052..a93a238ce2 100755 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -95,7 +95,6 @@ export class Config { async3: group({ ...resolvedConfig.rules, ...resolvedConfig.async3Rules }), arazzo1: group({ ...resolvedConfig.rules, ...resolvedConfig.arazzo1Rules }), overlay1: group({ ...resolvedConfig.rules, ...resolvedConfig.overlay1Rules }), - catalogEntity1: group({}), }; this.preprocessors = { @@ -128,7 +127,6 @@ export class Config { ...resolvedConfig.preprocessors, ...resolvedConfig.overlay1Preprocessors, }, - catalogEntity1: {}, }; this.decorators = { @@ -143,7 +141,6 @@ export class Config { ...resolvedConfig.decorators, ...resolvedConfig.overlay1Decorators, }, - catalogEntity1: {}, }; this.resolveIgnore(getIgnoreFilePath(opts.configPath)); @@ -267,8 +264,6 @@ export class Config { if (!plugin.typeExtension.overlay1) continue; extendedTypes = plugin.typeExtension.overlay1(extendedTypes, version); break; - case 'catalogEntity1': - continue; default: throw new Error('Not implemented'); } @@ -402,8 +397,6 @@ export class Config { (p) => p.decorators?.overlay1 && overlay1Rules.push(p.decorators.overlay1) ); return overlay1Rules; - case 'catalogEntity1': - return []; } } diff --git a/packages/core/src/detect-spec.ts b/packages/core/src/detect-spec.ts index 5b2279fe5a..f63b989f1a 100644 --- a/packages/core/src/detect-spec.ts +++ b/packages/core/src/detect-spec.ts @@ -12,7 +12,6 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', - 'catalogEntity1', ] as const; export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { @@ -26,8 +25,6 @@ export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion { return 'arazzo1'; } else if (version === 'overlay1') { return 'overlay1'; - } else if (version === 'catalogEntity1') { - return 'catalogEntity1'; } else { return 'oas3'; } diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index ab96e4389f..fb1b8abb43 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -11,7 +11,7 @@ import { createConfigTypes } from './types/redocly-yaml.js'; import { createEntityTypes } from './types/entity-yaml.js'; import { Struct } from './rules/common/struct.js'; import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js'; -import { EntityKeyValid } from './rules/catalogEntity1/entity-key-valid.js'; +import { EntityKeyValid } from './rules/common/entity-key-valid.js'; import { type Config } from './config/index.js'; import { isPlainObject } from './utils/is-plain-object.js'; @@ -220,7 +220,7 @@ export async function lintEntityFile(opts: { const ctx: WalkContext = { problems: [], - specVersion: 'catalogEntity1', + specVersion: 'oas3_0', config, visitorsData: {}, }; diff --git a/packages/core/src/oas-types.ts b/packages/core/src/oas-types.ts index f2e1597b28..8ffc94ca9b 100644 --- a/packages/core/src/oas-types.ts +++ b/packages/core/src/oas-types.ts @@ -40,18 +40,10 @@ export const specVersions = [ 'async3', 'arazzo1', 'overlay1', - 'catalogEntity1', ] as const; export type SpecVersion = typeof specVersions[number]; -export type SpecMajorVersion = - | 'oas2' - | 'oas3' - | 'async2' - | 'async3' - | 'arazzo1' - | 'overlay1' - | 'catalogEntity1'; +export type SpecMajorVersion = 'oas2' | 'oas3' | 'async2' | 'async3' | 'arazzo1' | 'overlay1'; const typesMap = { oas2: Oas2Types, @@ -114,8 +106,5 @@ export type Arazzo1DecoratorsSet = Record; export type Overlay1DecoratorsSet = Record; export function getTypes(spec: SpecVersion) { - if (spec === 'catalogEntity1') { - return {}; - } return typesMap[spec]; } diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index 161a7a9362..c61870ad18 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -5,7 +5,7 @@ import { createEntityTypes } from '../../../types/entity-yaml.js'; import { normalizeTypes } from '../../../types/index.js'; import { normalizeVisitors } from '../../../visitors.js'; import { walkDocument } from '../../../walk.js'; -import { EntityKeyValid } from '../../catalogEntity1/entity-key-valid.js'; +import { EntityKeyValid } from '../entity-key-valid.js'; import type { WalkContext } from '../../../walk.js'; const entityFileSchema = { diff --git a/packages/core/src/rules/catalogEntity1/entity-key-valid.ts b/packages/core/src/rules/common/entity-key-valid.ts similarity index 91% rename from packages/core/src/rules/catalogEntity1/entity-key-valid.ts rename to packages/core/src/rules/common/entity-key-valid.ts index de056d2831..8d2be2f495 100644 --- a/packages/core/src/rules/catalogEntity1/entity-key-valid.ts +++ b/packages/core/src/rules/common/entity-key-valid.ts @@ -1,11 +1,11 @@ import type { UserContext } from '../../walk.js'; -import type { CatalogEntity1Rule } from '../../visitors.js'; +import type { Oas3Rule } from '../../visitors.js'; const validKeyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const MIN_KEY_LENGTH = 2; const MAX_KEY_LENGTH = 150; -export const EntityKeyValid: CatalogEntity1Rule = () => { +export const EntityKeyValid: Oas3Rule = () => { return { any(node: any, { report, location }: UserContext) { if (typeof node === 'object' && node !== null && 'key' in node) { diff --git a/packages/core/src/visitors.ts b/packages/core/src/visitors.ts index a6632ee268..1958872f3c 100644 --- a/packages/core/src/visitors.ts +++ b/packages/core/src/visitors.ts @@ -377,7 +377,6 @@ export type Async2Rule = (options: Record) => Async2Visitor | Async export type Async3Rule = (options: Record) => Async3Visitor | Async3Visitor[]; export type Arazzo1Rule = (options: Record) => Arazzo1Visitor | Arazzo1Visitor[]; export type Overlay1Rule = (options: Record) => Overlay1Visitor | Overlay1Visitor[]; -export type CatalogEntity1Rule = (options: Record) => BaseVisitor | BaseVisitor[]; export type Oas3Preprocessor = (options: Record) => Oas3Visitor; export type Oas2Preprocessor = (options: Record) => Oas2Visitor; export type Async2Preprocessor = (options: Record) => Async2Visitor; From 456b5cb565ee16e34d4880d4b4e4df17601105cb Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Fri, 24 Oct 2025 14:47:31 +0200 Subject: [PATCH 03/19] chore: update redocly/config --- .changeset/floppy-rules-like.md | 5 +++++ package-lock.json | 16 ++++++++-------- packages/core/package.json | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/floppy-rules-like.md diff --git a/.changeset/floppy-rules-like.md b/.changeset/floppy-rules-like.md new file mode 100644 index 0000000000..ed939e0ae7 --- /dev/null +++ b/.changeset/floppy-rules-like.md @@ -0,0 +1,5 @@ +--- +"@redocly/openapi-core": patch +--- + +Updated @redocly/config to v0.36.2. diff --git a/package-lock.json b/package-lock.json index 28ad56d0fd..1c429d0b58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3514,9 +3514,9 @@ "link": true }, "node_modules/@redocly/config": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.31.0.tgz", - "integrity": "sha512-KPm2v//zj7qdGvClX0YqRNLQ9K7loVJWFEIceNxJIYPXP4hrhNvOLwjmxIkdkai0SdqYqogR2yjM/MjF9/AGdQ==", + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.36.2.tgz", + "integrity": "sha512-gwIjFOzyq9bdJCdtEHDMf2hEJ9hHXUHkY1U3IPWyZmRoFcibzlpXOLrT0xVgnn10IEttMwD+SZtZBE593yDXsw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "2.7.2" @@ -13227,7 +13227,7 @@ "license": "MIT", "dependencies": { "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.31.0", + "@redocly/config": "^0.36.2", "ajv-formats": "^2.1.1", "colorette": "^1.2.0", "js-levenshtein": "^1.1.6", @@ -15724,9 +15724,9 @@ } }, "@redocly/config": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.31.0.tgz", - "integrity": "sha512-KPm2v//zj7qdGvClX0YqRNLQ9K7loVJWFEIceNxJIYPXP4hrhNvOLwjmxIkdkai0SdqYqogR2yjM/MjF9/AGdQ==", + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.36.2.tgz", + "integrity": "sha512-gwIjFOzyq9bdJCdtEHDMf2hEJ9hHXUHkY1U3IPWyZmRoFcibzlpXOLrT0xVgnn10IEttMwD+SZtZBE593yDXsw==", "requires": { "json-schema-to-ts": "2.7.2" }, @@ -15752,7 +15752,7 @@ "version": "file:packages/core", "requires": { "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.31.0", + "@redocly/config": "^0.36.2", "@types/js-levenshtein": "^1.1.0", "@types/js-yaml": "^4.0.3", "@types/picomatch": "^4.0.2", diff --git a/packages/core/package.json b/packages/core/package.json index a18edc3188..b4529f19c4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,7 +53,7 @@ ], "dependencies": { "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.31.0", + "@redocly/config": "^0.36.2", "ajv-formats": "^2.1.1", "colorette": "^1.2.0", "js-levenshtein": "^1.1.6", From 9379f7d87df3380714cba4aa6ab471858a80abf8 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Fri, 24 Oct 2025 14:48:36 +0200 Subject: [PATCH 04/19] tests: update lint tests --- packages/core/src/__tests__/lint.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index 884d515269..2bf2beb376 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -5,12 +5,15 @@ import { BaseResolver } from '../resolve.js'; import { createConfig, loadConfig } from '../config/load.js'; import { parseYamlToDocument, replaceSourceWithRef } from '../../__tests__/utils.js'; import { detectSpec } from '../detect-spec.js'; -import { rootRedoclyConfigSchema } from '@redocly/config'; +import { + rootRedoclyConfigSchema, + entityFileDefaultSchema, + entityFileSchema, +} from '@redocly/config'; import { createConfigTypes } from '../types/redocly-yaml.js'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import { lintEntityFile } from '../lint.js'; -import { entityFileSchema, entityFileDefaultSchema } from './consts.js'; import { makeDocumentFromString } from '../resolve.js'; import { type Config } from '../config/index.js'; @@ -516,6 +519,7 @@ describe('lint', () => { "apis", "seo", "sso", + "mcp", "env", ], }, From 20187e5584bbd8e5a6014670ea5e80454bf398a5 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Fri, 24 Oct 2025 14:55:47 +0200 Subject: [PATCH 05/19] chore: remove unnecessary files --- .../local-json-server.test.ts.snap | 74 ++-- packages/core/src/__tests__/consts.ts | 356 ------------------ 2 files changed, 36 insertions(+), 394 deletions(-) delete mode 100644 packages/core/src/__tests__/consts.ts diff --git a/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap b/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap index dbb0d86fce..bce259099a 100644 --- a/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap +++ b/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap @@ -5,70 +5,68 @@ exports[`local-json-server > local-json-server test case 1`] = ` Running workflow local-json-server.arazzo.yaml / before - ✓ GET /items - step get-all-items-before -    ✓ status code check - $statusCode in [200] -    ✓ content-type check -    ✓ schema check + ✗ GET /items - step get-all-items-before +    ✗ status code check ──────────────────────────────────────────────────────────────────────────────── Running workflow local-json-server.arazzo.yaml / crud-items - ✓ POST /items - step add-item -    ✓ success criteria check - $statusCode == 201 -    ✓ status code check - $statusCode in [201] -    ✓ content-type check -    ✓ schema check - - ✗ GET /items - step get-all-items-again -    ✗ success criteria check - $response.body#/0/value == 100 -    ✓ status code check - $statusCode in [200] -    ✓ content-type check -    ✓ schema check + ✗ POST /items - step add-item +    ✗ success criteria check - $statusCode == 201 +    ✗ status code check +    ✗ unexpected error ──────────────────────────────────────────────────────────────────────────────── Running workflow local-json-server.arazzo.yaml / auto-inherit - ✓ POST /items - step add-item-auto-inherit -    ✓ status code check - $statusCode in [201] -    ✓ content-type check -    ✓ schema check - - ✓ GET /items - step get-all-items -    ✓ status code check - $statusCode in [200] -    ✓ content-type check -    ✓ schema check - - ✓ GET /items/{id} - step get-item-by-id-auto-inherit -    ✓ status code check - $statusCode in [200, 404] -    ✓ content-type check - - ✓ DELETE /items/{id} - step drop-item-auto-inherit -    ✓ success criteria check - $statusCode == 200 -    ✓ status code check - $statusCode in [200] + ✗ POST /items - step add-item-auto-inherit +    ✗ status code check +    ✗ unexpected error   Failed tests info: +  Workflow name: before + +    stepId - get-all-items-before +    ✗ status code check +       +         Workflow name: crud-items -    stepId - get-all-items-again +    stepId - add-item     ✗ success criteria check -      Checking simple criteria: {"condition":"$response.body#/0/value == 100"} +      Checking simple criteria: {"condition":"$statusCode == 201"} +       +    ✗ status code check +       +       +    ✗ unexpected error +    Reason: Error in resolving runtime expression '$response.body#/id'. +    This could be because the expression references a value from a previous failed step, or is trying to reference a variable that hasn't been set. +  Workflow name: auto-inherit + +    stepId - add-item-auto-inherit +    ✗ status code check +              +    ✗ unexpected error +    Reason: Error in resolving runtime expression '$response.body#/id'. +    This could be because the expression references a value from a previous failed step, or is trying to reference a variable that hasn't been set.   Summary for local-json-server.arazzo.yaml    -  Workflows: 2 passed, 1 failed, 3 total -  Steps: 6 passed, 1 failed, 7 total -  Checks: 20 passed, 1 failed, 21 total +  Workflows: 3 failed, 3 total +  Steps: 3 failed, 3 total +  Checks: 6 failed, 6 total   Time: ms ┌───────────────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├───────────────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ -│ x local-json-server.arazzo.yaml │ 3 │ 2 │ 1 │ - │ +│ x local-json-server.arazzo.yaml │ 3 │ 0 │ 3 │ - │ └───────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘ diff --git a/packages/core/src/__tests__/consts.ts b/packages/core/src/__tests__/consts.ts deleted file mode 100644 index f3a5c5e43b..0000000000 --- a/packages/core/src/__tests__/consts.ts +++ /dev/null @@ -1,356 +0,0 @@ -export const ENTITY_RELATION_TYPES = [ - 'partOf', - 'hasParts', - 'creates', - 'createdBy', - 'owns', - 'ownedBy', - 'implements', - 'implementedBy', - 'dependsOn', - 'dependencyOf', - 'uses', - 'usedBy', - 'produces', - 'consumes', - 'linksTo', - 'supersedes', - 'supersededBy', - 'compatibleWith', - 'extends', - 'extendedBy', - 'relatesTo', - 'hasMember', - 'memberOf', - 'triggers', - 'triggeredBy', - 'returns', - 'returnedBy', -] as const; - -export const userMetadataSchema = { - type: 'object', - properties: { - email: { - type: 'string', - description: 'Email of the user', - }, - }, - required: ['email'], - additionalProperties: true, -} as const; - -export const apiDescriptionMetadataSchema = { - type: 'object', - properties: { - specType: { - type: 'string', - enum: ['jsonschema', 'openapi', 'asyncapi', 'avro', 'zod', 'graphql', 'protobuf', 'arazzo'], - description: 'Type of the API description', - }, - descriptionFile: { - type: 'string', - description: 'Path to the file containing the API description', - }, - }, - required: ['specType', 'descriptionFile'], - additionalProperties: true, -} as const; - -export const apiOperationMetadataSchema = { - type: 'object', - properties: { - method: { - type: 'string', - enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'MUTATION', 'QUERY', 'SUBSCRIBE', 'PUBLISH'], - description: 'HTTP method of the API operation', - }, - path: { - type: 'string', - description: 'Path of the API operation', - }, - payload: { - type: 'array', - items: { - type: 'string', - description: 'Related dataSchema name', - }, - }, - responses: { - type: 'array', - items: { - type: 'string', - description: 'Related dataSchema name', - }, - }, - }, - required: ['method', 'path'], - additionalProperties: true, -} as const; - -export const dataSchemaMetadataSchema = { - type: 'object', - properties: { - specType: { - type: 'string', - enum: ['jsonschema', 'openapi', 'asyncapi', 'avro', 'zod', 'graphql', 'protobuf', 'arazzo'], - description: 'Specification type of the data schema', - }, - schema: { - type: 'string', - description: 'Inline schema of the data structure', - }, - sdl: { - type: 'string', - description: 'SDL of the data structure', - }, - }, - required: ['specType'], - // oneOf: [{ required: ['schema'] }, { required: ['sdl'] }], - additionalProperties: true, -} as const; - -export const defaultMetadataSchema = { - type: 'object', - additionalProperties: true, -} as const; - -export const slackChannelFileSchema = { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 2, - maxLength: 150, - }, - }, - required: ['name'], - additionalProperties: false, -} as const; - -export const slackContactFileSchema = { - type: 'object', - properties: { - channels: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 2, - maxLength: 150, - }, - }, - required: ['name'], - additionalProperties: false, - }, - }, - }, - required: ['channels'], - additionalProperties: false, -} as const; - -export const entityContactFileSchema = { - type: 'object', - properties: { - slack: { - type: 'object', - properties: { - channels: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 2, - maxLength: 150, - }, - url: { - type: 'string', - }, - }, - required: ['name'], - additionalProperties: false, - }, - }, - }, - required: ['channels'], - additionalProperties: false, - }, - }, - additionalProperties: false, -} as const; - -export const entityLinkFileSchema = { - type: 'object', - properties: { - label: { - type: 'string', - minLength: 2, - maxLength: 150, - }, - url: { - type: 'string', - }, - }, - required: ['label', 'url'], - additionalProperties: false, -} as const; - -export const entityRelationFileSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: ENTITY_RELATION_TYPES, - }, - key: { - type: 'string', - minLength: 2, - maxLength: 100, - }, - }, - required: ['type', 'key'], - additionalProperties: false, -} as const; - -// Base entity schema properties -export const entityBaseProperties = { - key: { - type: 'string', - pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', - minLength: 2, - maxLength: 150, - }, - type: { - type: 'string', - enum: ['user', 'data-schema', 'api-operation', 'api-description', 'service', 'domain', 'team'], - }, - title: { - type: 'string', - minLength: 2, - maxLength: 200, - }, - summary: { - type: 'string', - minLength: 1, - maxLength: 500, - }, - tags: { - type: 'array', - items: { - type: 'string', - minLength: 1, - maxLength: 50, - }, - }, - git: { - type: 'array', - items: { - type: 'string', - }, - }, - contact: { - type: 'object', - additionalProperties: true, - }, - links: { - type: 'array', - items: entityLinkFileSchema, - }, - relations: { - type: 'array', - items: entityRelationFileSchema, - }, - metadata: { - type: 'object', - additionalProperties: true, - }, -} as const; - -export const entityFileSchema = { - type: 'object', - discriminator: { - propertyName: 'type', - }, - oneOf: [ - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'user' }, - metadata: userMetadataSchema, - }, - required: ['key', 'title', 'type', 'metadata'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'api-operation' }, - metadata: apiOperationMetadataSchema, - }, - required: ['key', 'title', 'type', 'metadata'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'data-schema' }, - metadata: dataSchemaMetadataSchema, - }, - required: ['key', 'title', 'type', 'metadata'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'api-description' }, - metadata: apiDescriptionMetadataSchema, - }, - required: ['key', 'title', 'type', 'metadata'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'service' }, - }, - required: ['key', 'title', 'type'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'domain' }, - }, - required: ['key', 'title', 'type'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - ...entityBaseProperties, - type: { const: 'team' }, - }, - required: ['key', 'title', 'type'], - additionalProperties: false, - }, - ], -} as const; - -export const entityFileDefaultSchema = { - type: 'object', - properties: { - ...entityBaseProperties, - }, - required: ['key', 'title', 'type'], - additionalProperties: false, -} as const; From a0715401458896d229539cd7a17687845ed706b2 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Fri, 24 Oct 2025 15:00:19 +0200 Subject: [PATCH 06/19] tests: update entity-yaml test imports --- packages/core/src/__tests__/entity-yaml.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts index ad9fee59b9..2c12b593a9 100644 --- a/packages/core/src/__tests__/entity-yaml.test.ts +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { createEntityTypes } from '../types/entity-yaml.js'; import { normalizeTypes } from '../types/index.js'; -import { entityFileSchema, entityFileDefaultSchema } from './consts.js'; +import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; describe('entity-yaml', () => { it('should create entity types with discriminator', () => { From 656df7617dda774964b30924431313ea2bc00818 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Fri, 24 Oct 2025 15:09:53 +0200 Subject: [PATCH 07/19] tests: revert snapshot --- .../local-json-server.test.ts.snap | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap b/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap index bce259099a..dbb0d86fce 100644 --- a/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap +++ b/__tests__/respect/local-json-server/__snapshots__/local-json-server.test.ts.snap @@ -5,68 +5,70 @@ exports[`local-json-server > local-json-server test case 1`] = ` Running workflow local-json-server.arazzo.yaml / before - ✗ GET /items - step get-all-items-before -    ✗ status code check + ✓ GET /items - step get-all-items-before +    ✓ status code check - $statusCode in [200] +    ✓ content-type check +    ✓ schema check ──────────────────────────────────────────────────────────────────────────────── Running workflow local-json-server.arazzo.yaml / crud-items - ✗ POST /items - step add-item -    ✗ success criteria check - $statusCode == 201 -    ✗ status code check -    ✗ unexpected error + ✓ POST /items - step add-item +    ✓ success criteria check - $statusCode == 201 +    ✓ status code check - $statusCode in [201] +    ✓ content-type check +    ✓ schema check + + ✗ GET /items - step get-all-items-again +    ✗ success criteria check - $response.body#/0/value == 100 +    ✓ status code check - $statusCode in [200] +    ✓ content-type check +    ✓ schema check ──────────────────────────────────────────────────────────────────────────────── Running workflow local-json-server.arazzo.yaml / auto-inherit - ✗ POST /items - step add-item-auto-inherit -    ✗ status code check -    ✗ unexpected error + ✓ POST /items - step add-item-auto-inherit +    ✓ status code check - $statusCode in [201] +    ✓ content-type check +    ✓ schema check + + ✓ GET /items - step get-all-items +    ✓ status code check - $statusCode in [200] +    ✓ content-type check +    ✓ schema check + + ✓ GET /items/{id} - step get-item-by-id-auto-inherit +    ✓ status code check - $statusCode in [200, 404] +    ✓ content-type check + + ✓ DELETE /items/{id} - step drop-item-auto-inherit +    ✓ success criteria check - $statusCode == 200 +    ✓ status code check - $statusCode in [200]   Failed tests info: -  Workflow name: before - -    stepId - get-all-items-before -    ✗ status code check -       -         Workflow name: crud-items -    stepId - add-item +    stepId - get-all-items-again     ✗ success criteria check -      Checking simple criteria: {"condition":"$statusCode == 201"} -       -    ✗ status code check -       -       -    ✗ unexpected error -    Reason: Error in resolving runtime expression '$response.body#/id'. -    This could be because the expression references a value from a previous failed step, or is trying to reference a variable that hasn't been set. -  Workflow name: auto-inherit - -    stepId - add-item-auto-inherit -    ✗ status code check -       +      Checking simple criteria: {"condition":"$response.body#/0/value == 100"}        -    ✗ unexpected error -    Reason: Error in resolving runtime expression '$response.body#/id'. -    This could be because the expression references a value from a previous failed step, or is trying to reference a variable that hasn't been set.   Summary for local-json-server.arazzo.yaml    -  Workflows: 3 failed, 3 total -  Steps: 3 failed, 3 total -  Checks: 6 failed, 6 total +  Workflows: 2 passed, 1 failed, 3 total +  Steps: 6 passed, 1 failed, 7 total +  Checks: 20 passed, 1 failed, 21 total   Time: ms ┌───────────────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├───────────────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ -│ x local-json-server.arazzo.yaml │ 3 │ 0 │ 3 │ - │ +│ x local-json-server.arazzo.yaml │ 3 │ 2 │ 1 │ - │ └───────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘ From 39b1f48bedee763bf86f9c7ca1df5c873b43642e Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Tue, 28 Oct 2025 10:17:21 +0100 Subject: [PATCH 08/19] tests: update entity-yaml test file --- packages/core/src/__tests__/entity-yaml.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts index 2c12b593a9..6dc3f6cae7 100644 --- a/packages/core/src/__tests__/entity-yaml.test.ts +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -41,7 +41,6 @@ describe('entity-yaml', () => { } } - // Test default resolution when no type if (typeof entityFileArrayNode.items === 'function') { const unknownEntity = { key: 'unknown', title: 'Unknown' }; const resolvedType = entityFileArrayNode.items(unknownEntity, 'root'); @@ -57,7 +56,6 @@ describe('entity-yaml', () => { const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); const normalizedTypes = normalizeTypes(entityTypes); - // Test that user type has correct required fields const userNode = normalizedTypes['user']; if (userNode && Array.isArray(userNode.required)) { expect(userNode.required).toContain('key'); From bcd91fbed7a64d4d73416f3fdce41e0373dc8eb8 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Tue, 28 Oct 2025 14:31:40 +0100 Subject: [PATCH 09/19] chore: update entity-key-valid test --- .../common/__tests__/entity-key-valid.test.ts | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index c61870ad18..3b04c39702 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -7,36 +7,7 @@ import { normalizeVisitors } from '../../../visitors.js'; import { walkDocument } from '../../../walk.js'; import { EntityKeyValid } from '../entity-key-valid.js'; import type { WalkContext } from '../../../walk.js'; - -const entityFileSchema = { - type: 'object', - properties: { - type: { type: 'string' }, - key: { - type: 'string', - pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', - minLength: 2, - maxLength: 150, - }, - title: { type: 'string' }, - }, - required: ['type', 'key', 'title'], -} as const; - -const entityFileDefaultSchema = { - type: 'object', - properties: { - type: { type: 'string' }, - key: { - type: 'string', - pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', - minLength: 2, - maxLength: 150, - }, - title: { type: 'string' }, - }, - required: ['type', 'key', 'title'], -} as const; +import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; function lintEntityKey(source: string): WalkContext['problems'] { const document = makeDocumentFromString(source, '/test.yaml'); From 10ddef3d9fe88d3751058e8325530ed71f33c30f Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 08:10:19 +0100 Subject: [PATCH 10/19] chore: apply review suggestions --- .../core/src/__tests__/entity-yaml.test.ts | 2 +- packages/core/src/__tests__/lint.test.ts | 144 +++++++++--------- packages/core/src/lint.ts | 7 +- .../core/src/rules/common/entity-key-valid.ts | 4 +- packages/core/src/types/entity-yaml.ts | 4 +- 5 files changed, 80 insertions(+), 81 deletions(-) diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts index 6dc3f6cae7..3ff20876ef 100644 --- a/packages/core/src/__tests__/entity-yaml.test.ts +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -44,7 +44,7 @@ describe('entity-yaml', () => { if (typeof entityFileArrayNode.items === 'function') { const unknownEntity = { key: 'unknown', title: 'Unknown' }; const resolvedType = entityFileArrayNode.items(unknownEntity, 'root'); - console.log('Resolved type for entity without type:', resolvedType); + expect(resolvedType).toBeTruthy(); if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { expect(resolvedType.name).toBe('EntityFileDefault'); diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index 2bf2beb376..381e19111e 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -1971,16 +1971,16 @@ describe('lint', () => { describe('lintEntityFile', () => { it('should lint a valid user entity file', async () => { - const entityYaml = ` - type: user - key: john-doe - title: John Doe - summary: Senior Software Engineer - metadata: - email: john@example.com - tags: - - engineering - `; + const entityYaml = outdent` + type: user + key: john-doe + title: John Doe + summary: Senior Software Engineer + metadata: + email: john@example.com + tags: + - engineering + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -1994,13 +1994,13 @@ describe('lint', () => { }); it('should detect missing required fields in entity', async () => { - const entityYaml = ` - type: user - key: john-doe - # Missing required 'title' field - metadata: - email: john@example.com - `; + const entityYaml = outdent` + type: user + key: john-doe + # Missing required 'title' field + metadata: + email: john@example.com + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2047,11 +2047,11 @@ describe('lint', () => { }); it('should use default schema when type is missing', async () => { - const entityYaml = ` - key: unknown-entity - title: Unknown Entity - summary: An entity without a type field - `; + const entityYaml = outdent` + key: unknown-entity + title: Unknown Entity + summary: An entity without a type field + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2066,13 +2066,13 @@ describe('lint', () => { }); it('should detect missing metadata.email for user entity', async () => { - const entityYaml = ` - type: user - key: john-doe - title: John Doe - metadata: - name: John - `; + const entityYaml = outdent` + type: user + key: john-doe + title: John Doe + metadata: + name: John + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2087,11 +2087,11 @@ describe('lint', () => { }); it('should detect missing metadata for user entity', async () => { - const entityYaml = ` - type: user - key: john-doe - title: John Doe - `; + const entityYaml = outdent` + type: user + key: john-doe + title: John Doe + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2106,11 +2106,11 @@ describe('lint', () => { }); it('should not validate patterns with Struct rule', async () => { - const entityYaml = ` - type: service - key: Invalid_Key_With_Underscores - title: Invalid Service - `; + const entityYaml = outdent` + type: service + key: Invalid_Key_With_Underscores + title: Invalid Service + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2126,14 +2126,14 @@ describe('lint', () => { }); it('should detect missing metadata fields for specific entity types', async () => { - const entityYaml = ` - type: api-description - key: my-api - title: My API - metadata: - specType: openapi - # Missing descriptionFile - `; + const entityYaml = outdent` + type: api-description + key: my-api + title: My API + metadata: + specType: openapi + # Missing descriptionFile + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2147,17 +2147,17 @@ describe('lint', () => { }); it('should detect invalid entity type in array', async () => { - const entitiesYaml = ` - - type: user - key: john-doe - title: John Doe - metadata: - email: john@example.com - - - type: service - key: invalid-service - # Missing required title field - `; + const entitiesYaml = outdent` + - type: user + key: john-doe + title: John Doe + metadata: + email: john@example.com + + - type: service + key: invalid-service + # Missing required title field + `; const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); @@ -2172,12 +2172,12 @@ describe('lint', () => { }); it('should validate service entity without metadata', async () => { - const entityYaml = ` - type: service - key: my-service - title: My Service - summary: A simple service - `; + const entityYaml = outdent` + type: service + key: my-service + title: My Service + summary: A simple service + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); @@ -2191,14 +2191,14 @@ describe('lint', () => { }); it('should detect invalid relation type', async () => { - const entityYaml = ` - type: service - key: my-service - title: My Service - relations: - - type: invalidRelationType - key: some-entity - `; + const entityYaml = outdent` + type: service + key: my-service + title: My Service + relations: + - type: invalidRelationType + key: some-entity + `; const document = makeDocumentFromString(entityYaml, '/entity.yaml'); diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index fb1b8abb43..88f8752a1b 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -5,10 +5,10 @@ import { walkDocument } from './walk.js'; import { initRules } from './config/rules.js'; import { normalizeTypes } from './types/index.js'; import { releaseAjvInstance } from './rules/ajv.js'; -import { getTypes } from './oas-types.js'; +import { getTypes, type SpecVersion } from './oas-types.js'; import { detectSpec, getMajorSpecVersion } from './detect-spec.js'; import { createConfigTypes } from './types/redocly-yaml.js'; -import { createEntityTypes } from './types/entity-yaml.js'; +import { createEntityTypes, ENTITY_DISCRIMINATOR_NAME } from './types/entity-yaml.js'; import { Struct } from './rules/common/struct.js'; import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js'; import { EntityKeyValid } from './rules/common/entity-key-valid.js'; @@ -220,7 +220,7 @@ export async function lintEntityFile(opts: { const ctx: WalkContext = { problems: [], - specVersion: 'oas3_0', + specVersion: 'entity' as SpecVersion, config, visitorsData: {}, }; @@ -232,7 +232,6 @@ export async function lintEntityFile(opts: { if (Array.isArray(config.document.parsed)) { rootType = types.EntityFileArray; } else if (isPlainObject(config.document.parsed)) { - const ENTITY_DISCRIMINATOR_NAME = 'type'; const typeValue = (config.document.parsed as Record)[ ENTITY_DISCRIMINATOR_NAME ]; diff --git a/packages/core/src/rules/common/entity-key-valid.ts b/packages/core/src/rules/common/entity-key-valid.ts index 8d2be2f495..6faaf649c0 100644 --- a/packages/core/src/rules/common/entity-key-valid.ts +++ b/packages/core/src/rules/common/entity-key-valid.ts @@ -21,14 +21,14 @@ export const EntityKeyValid: Oas3Rule = () => { if (key.length < MIN_KEY_LENGTH) { report({ - message: `Entity \`key\` must be at least ${MIN_KEY_LENGTH} characters long.`, + message: `Entity 'key' must be at least ${MIN_KEY_LENGTH} characters long.`, location: location.child(['key']), }); } if (key.length > MAX_KEY_LENGTH) { report({ - message: `Entity \`key\` must not exceed ${MAX_KEY_LENGTH} characters.`, + message: `Entity 'key' must not exceed ${MAX_KEY_LENGTH} characters.`, location: location.child(['key']), }); } diff --git a/packages/core/src/types/entity-yaml.ts b/packages/core/src/types/entity-yaml.ts index 5236be7286..1e4d2b872a 100644 --- a/packages/core/src/types/entity-yaml.ts +++ b/packages/core/src/types/entity-yaml.ts @@ -4,12 +4,12 @@ import { isPlainObject } from '../utils/is-plain-object.js'; import type { JSONSchema } from 'json-schema-to-ts'; import type { NodeType } from './index.js'; +export const ENTITY_DISCRIMINATOR_NAME = 'type'; + export function createEntityTypes( entitySchema: JSONSchema, entityDefaultSchema: JSONSchema ): Record { - const ENTITY_DISCRIMINATOR_NAME = 'type'; - const defaultNodeTypes = getNodeTypesFromJSONSchema('EntityFileDefault', entityDefaultSchema); const namedNodeTypes = getNodeTypesFromJSONSchema('EntityFile', entitySchema); From ae71ae87a35cfc0157181e0cbaa82b47599fe59f Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 08:17:08 +0100 Subject: [PATCH 11/19] chore: apply review suggestions --- .../core/src/rules/common/__tests__/entity-key-valid.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index 3b04c39702..e7309295be 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -8,6 +8,7 @@ import { walkDocument } from '../../../walk.js'; import { EntityKeyValid } from '../entity-key-valid.js'; import type { WalkContext } from '../../../walk.js'; import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; +import { SpecVersion } from '../../../oas-types.js'; function lintEntityKey(source: string): WalkContext['problems'] { const document = makeDocumentFromString(source, '/test.yaml'); @@ -16,7 +17,7 @@ function lintEntityKey(source: string): WalkContext['problems'] { const ctx: WalkContext = { problems: [], - specVersion: 'oas3_0', + specVersion: 'entity' as SpecVersion, config: {} as Config, visitorsData: {}, }; From 09f88e3dd07d9edada69054b05d52af00825baa1 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 08:46:15 +0100 Subject: [PATCH 12/19] chore: add fixme for entity lint methods --- packages/core/src/lint.ts | 2 +- .../core/src/rules/common/__tests__/entity-key-valid.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index 88f8752a1b..2003f3ce48 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -220,7 +220,7 @@ export async function lintEntityFile(opts: { const ctx: WalkContext = { problems: [], - specVersion: 'entity' as SpecVersion, + specVersion: 'entity' as SpecVersion, // FIXME: this should be proper SpecVersion config, visitorsData: {}, }; diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index e7309295be..c3afc61774 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -17,7 +17,7 @@ function lintEntityKey(source: string): WalkContext['problems'] { const ctx: WalkContext = { problems: [], - specVersion: 'entity' as SpecVersion, + specVersion: 'entity' as SpecVersion, // FIXME: this should be proper SpecVersion config: {} as Config, visitorsData: {}, }; From 7244ff0737fbd61b54c5bafcec83c87176c17dc2 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 14:26:18 +0100 Subject: [PATCH 13/19] chore: apply review suggestions --- .../core/src/__tests__/entity-yaml.test.ts | 50 ++++++++++--------- packages/core/src/lint.ts | 4 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts index 3ff20876ef..f1d9a39159 100644 --- a/packages/core/src/__tests__/entity-yaml.test.ts +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -1,8 +1,7 @@ import { describe, it, expect } from 'vitest'; import { createEntityTypes } from '../types/entity-yaml.js'; -import { normalizeTypes } from '../types/index.js'; +import { normalizeTypes, ResolveTypeFn } from '../types/index.js'; import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; - describe('entity-yaml', () => { it('should create entity types with discriminator', () => { const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); @@ -18,36 +17,39 @@ describe('entity-yaml', () => { expect(entityTypes).toHaveProperty('EntityFileArray'); }); - it('should resolve entity type based on discriminator for array items', () => { + it('should resolve entity type to default for array items', () => { const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); const normalizedTypes = normalizeTypes(entityTypes); const entityFileArrayNode = normalizedTypes['EntityFileArray']; - if (typeof entityFileArrayNode.items === 'function') { - const userEntity = { - type: 'user', - key: 'john-doe', - title: 'John Doe', - metadata: { email: 'john@example.com' }, - }; - const resolvedType = entityFileArrayNode.items(userEntity, 'root'); - expect(resolvedType).toBeTruthy(); - if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { - expect(resolvedType.name).toBe('user'); - if ('properties' in resolvedType) { - expect(resolvedType.properties).toHaveProperty('metadata'); - } - } + const unknownEntity = { key: 'unknown', title: 'Unknown' }; + const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)(unknownEntity, 'root'); + + expect(resolvedType).toBeTruthy(); + if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { + expect(resolvedType.name).toBe('EntityFileDefault'); } + }); - if (typeof entityFileArrayNode.items === 'function') { - const unknownEntity = { key: 'unknown', title: 'Unknown' }; - const resolvedType = entityFileArrayNode.items(unknownEntity, 'root'); + it('should resolve entity type based on discriminator for array items', () => { + const entityTypes = createEntityTypes(entityFileSchema, entityFileDefaultSchema); + const normalizedTypes = normalizeTypes(entityTypes); + + const entityFileArrayNode = normalizedTypes['EntityFileArray']; + const userEntity = { + type: 'user', + key: 'john-doe', + title: 'John Doe', + metadata: { email: 'john@example.com' }, + }; + const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)(userEntity, 'root'); - expect(resolvedType).toBeTruthy(); - if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { - expect(resolvedType.name).toBe('EntityFileDefault'); + expect(resolvedType).toBeTruthy(); + if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { + expect(resolvedType.name).toBe('user'); + if ('properties' in resolvedType) { + expect(resolvedType.properties).toHaveProperty('metadata'); } } }); diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index 2003f3ce48..6fb79fdfeb 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -232,9 +232,7 @@ export async function lintEntityFile(opts: { if (Array.isArray(config.document.parsed)) { rootType = types.EntityFileArray; } else if (isPlainObject(config.document.parsed)) { - const typeValue = (config.document.parsed as Record)[ - ENTITY_DISCRIMINATOR_NAME - ]; + const typeValue = config.document.parsed[ENTITY_DISCRIMINATOR_NAME]; if (typeof typeValue === 'string' && types[typeValue]) { rootType = types[typeValue]; } From 1817ef1cb14493aa61d11d99451ef400f62388aa Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 14:59:40 +0100 Subject: [PATCH 14/19] chore: apply review suggestions --- .../core/src/__tests__/entity-yaml.test.ts | 40 +++--- packages/core/src/__tests__/lint.test.ts | 128 +++++++++--------- 2 files changed, 82 insertions(+), 86 deletions(-) diff --git a/packages/core/src/__tests__/entity-yaml.test.ts b/packages/core/src/__tests__/entity-yaml.test.ts index f1d9a39159..34b1e5f4c9 100644 --- a/packages/core/src/__tests__/entity-yaml.test.ts +++ b/packages/core/src/__tests__/entity-yaml.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { createEntityTypes } from '../types/entity-yaml.js'; -import { normalizeTypes, ResolveTypeFn } from '../types/index.js'; +import { NormalizedNodeType, normalizeTypes, ResolveTypeFn } from '../types/index.js'; import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; describe('entity-yaml', () => { it('should create entity types with discriminator', () => { @@ -24,12 +24,13 @@ describe('entity-yaml', () => { const entityFileArrayNode = normalizedTypes['EntityFileArray']; const unknownEntity = { key: 'unknown', title: 'Unknown' }; - const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)(unknownEntity, 'root'); + const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)( + unknownEntity, + 'root' + ) as NormalizedNodeType; expect(resolvedType).toBeTruthy(); - if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { - expect(resolvedType.name).toBe('EntityFileDefault'); - } + expect(resolvedType.name).toBe('EntityFileDefault'); }); it('should resolve entity type based on discriminator for array items', () => { @@ -43,15 +44,14 @@ describe('entity-yaml', () => { title: 'John Doe', metadata: { email: 'john@example.com' }, }; - const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)(userEntity, 'root'); + const resolvedType = (entityFileArrayNode.items as ResolveTypeFn)( + userEntity, + 'root' + ) as NormalizedNodeType; expect(resolvedType).toBeTruthy(); - if (resolvedType && typeof resolvedType === 'object' && 'name' in resolvedType) { - expect(resolvedType.name).toBe('user'); - if ('properties' in resolvedType) { - expect(resolvedType.properties).toHaveProperty('metadata'); - } - } + expect(resolvedType.name).toBe('user'); + expect(resolvedType.properties).toHaveProperty('metadata'); }); it('should have correct required fields for entity types', () => { @@ -59,17 +59,13 @@ describe('entity-yaml', () => { const normalizedTypes = normalizeTypes(entityTypes); const userNode = normalizedTypes['user']; - if (userNode && Array.isArray(userNode.required)) { - expect(userNode.required).toContain('key'); - expect(userNode.required).toContain('title'); - expect(userNode.required).toContain('type'); - } + expect(userNode.required).toContain('key'); + expect(userNode.required).toContain('title'); + expect(userNode.required).toContain('type'); const defaultNode = normalizedTypes['EntityFileDefault']; - if (defaultNode && Array.isArray(defaultNode.required)) { - expect(defaultNode.required).toContain('type'); - expect(defaultNode.required).toContain('key'); - expect(defaultNode.required).toContain('title'); - } + expect(defaultNode.required).toContain('type'); + expect(defaultNode.required).toContain('key'); + expect(defaultNode.required).toContain('title'); }); }); diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index 381e19111e..e30fe5832e 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -2015,25 +2015,25 @@ describe('lint', () => { }); it('should lint array of entities', async () => { - const entitiesYaml = ` - - type: user - key: john-doe - title: John Doe - metadata: - email: john@example.com - - - type: service - key: api-service - title: API Service - summary: Core API service - - - type: api-description - key: users-api - title: Users API - metadata: - specType: openapi - descriptionFile: users-api.yaml - `; + const entitiesYaml = outdent` + - type: user + key: john-doe + title: John Doe + metadata: + email: john@example.com + + - type: service + key: api-service + title: API Service + summary: Core API service + + - type: api-description + key: users-api + title: Users API + metadata: + specType: openapi + descriptionFile: users-api.yaml + `; const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); @@ -2220,13 +2220,13 @@ describe('lint', () => { }); it('should validate type-specific metadata fields correctly', async () => { - const apiOperationYaml = ` - type: api-operation - key: test-operation - title: Test Operation - metadata: - wrongField: value - `; + const apiOperationYaml = outdent` + type: api-operation + key: test-operation + title: Test Operation + metadata: + wrongField: value + `; const document = makeDocumentFromString(apiOperationYaml, '/entity.yaml'); @@ -2247,28 +2247,28 @@ describe('lint', () => { }); it('should validate different metadata schemas in array of mixed entity types', async () => { - const mixedEntitiesYaml = ` - - type: api-description - key: my-api - title: My API - metadata: - specType: openapi - # Missing descriptionFile - - - type: api-operation - key: my-operation - title: My Operation - metadata: - wrongField: value - # Missing method and path - - - type: data-schema - key: my-schema - title: My Schema - metadata: - wrongField: value - # Missing specType - `; + const mixedEntitiesYaml = outdent` + - type: api-description + key: my-api + title: My API + metadata: + specType: openapi + # Missing descriptionFile + + - type: api-operation + key: my-operation + title: My Operation + metadata: + wrongField: value + # Missing method and path + + - type: data-schema + key: my-schema + title: My Schema + metadata: + wrongField: value + # Missing specType + `; const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); @@ -2298,22 +2298,22 @@ describe('lint', () => { }); it('should handle entity without type in array using default schema', async () => { - const mixedEntitiesYaml = ` - - type: user - key: valid-user - title: Valid User - metadata: - email: user@example.com - - - key: no-type-entity - title: Entity Without Type - summary: This entity is missing the type field - # Missing type - should use EntityFileDefault schema - - - type: service - key: valid-service - title: Valid Service - `; + const mixedEntitiesYaml = outdent` + - type: user + key: valid-user + title: Valid User + metadata: + email: user@example.com + + - key: no-type-entity + title: Entity Without Type + summary: This entity is missing the type field + # Missing type - should use EntityFileDefault schema + + - type: service + key: valid-service + title: Valid Service + `; const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); From d475d60ec581ab350b800da8db7a90db945e55b4 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 15:17:39 +0100 Subject: [PATCH 15/19] tests: add outdent to entity-key-valid test file --- .../common/__tests__/entity-key-valid.test.ts | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index c3afc61774..4e6a63176a 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -9,6 +9,7 @@ import { EntityKeyValid } from '../entity-key-valid.js'; import type { WalkContext } from '../../../walk.js'; import { entityFileSchema, entityFileDefaultSchema } from '@redocly/config'; import { SpecVersion } from '../../../oas-types.js'; +import { outdent } from 'outdent'; function lintEntityKey(source: string): WalkContext['problems'] { const document = makeDocumentFromString(source, '/test.yaml'); @@ -45,81 +46,81 @@ function lintEntityKey(source: string): WalkContext['problems'] { describe('EntityKeyValid rule', () => { it('should pass for valid entity key', () => { - const problems = lintEntityKey(` -type: user -key: valid-key-123 -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: valid-key-123 + title: Valid User + `); expect(problems).toHaveLength(0); }); it('should fail for key that is too short', () => { - const problems = lintEntityKey(` -type: user -key: a -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: a + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('at least 2 characters'); }); it('should fail for key with uppercase letters', () => { - const problems = lintEntityKey(` -type: user -key: Invalid-Key -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: Invalid-Key + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('lowercase letters'); }); it('should fail for key starting with hyphen', () => { - const problems = lintEntityKey(` -type: user -key: -invalid-key -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: -invalid-key + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('cannot start or end with a hyphen'); }); it('should fail for key ending with hyphen', () => { - const problems = lintEntityKey(` -type: user -key: invalid-key- -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: invalid-key- + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('cannot start or end with a hyphen'); }); it('should fail for key with invalid characters', () => { - const problems = lintEntityKey(` -type: user -key: invalid_key@ -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: invalid_key@ + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('lowercase letters'); }); it('should fail for key that exceeds maximum length', () => { const longKey = 'a'.repeat(151); - const problems = lintEntityKey(` -type: user -key: ${longKey} -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: ${longKey} + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('must not exceed 150 characters'); }); it('should fail for non-string key', () => { - const problems = lintEntityKey(` -type: user -key: 123 -title: Valid User -`); + const problems = lintEntityKey(outdent` + type: user + key: 123 + title: Valid User + `); expect(problems).toHaveLength(1); expect(problems[0].message).toContain('must be a string'); }); From 7d7d6813e14872cbd3bf124989906707b88e6981 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 15:51:08 +0100 Subject: [PATCH 16/19] chore: use document instead of whole config in lintEntityFile method --- packages/core/src/__tests__/lint.test.ts | 28 ++++++++++++------------ packages/core/src/lint.ts | 20 ++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index e30fe5832e..cc05390f13 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -1985,7 +1985,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2005,7 +2005,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2038,7 +2038,7 @@ describe('lint', () => { const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2056,7 +2056,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2077,7 +2077,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2096,7 +2096,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2115,7 +2115,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2138,7 +2138,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2162,7 +2162,7 @@ describe('lint', () => { const document = makeDocumentFromString(entitiesYaml, '/entities.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2182,7 +2182,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2203,7 +2203,7 @@ describe('lint', () => { const document = makeDocumentFromString(entityYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2231,7 +2231,7 @@ describe('lint', () => { const document = makeDocumentFromString(apiOperationYaml, '/entity.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2273,7 +2273,7 @@ describe('lint', () => { const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); @@ -2318,7 +2318,7 @@ describe('lint', () => { const document = makeDocumentFromString(mixedEntitiesYaml, '/entities.yaml'); const problems = await lintEntityFile({ - config: { document } as Config, + document, entitySchema: entityFileSchema, entityDefaultSchema: entityFileDefaultSchema, }); diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index 6fb79fdfeb..9a9322db5a 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -200,28 +200,22 @@ export async function lintConfig(opts: { } export async function lintEntityFile(opts: { - config: Config; + document: Document; entitySchema: JSONSchema; entityDefaultSchema: JSONSchema; severity?: ProblemSeverity; externalRefResolver?: BaseResolver; }) { const { - config, + document, entitySchema, entityDefaultSchema, severity, externalRefResolver = new BaseResolver(), } = opts; - - if (!config.document) { - throw new Error('Config document is not set.'); - } - const ctx: WalkContext = { problems: [], specVersion: 'entity' as SpecVersion, // FIXME: this should be proper SpecVersion - config, visitorsData: {}, }; @@ -229,10 +223,10 @@ export async function lintEntityFile(opts: { const types = normalizeTypes(entityTypes); let rootType = types.EntityFileDefault; - if (Array.isArray(config.document.parsed)) { + if (Array.isArray(document.parsed)) { rootType = types.EntityFileArray; - } else if (isPlainObject(config.document.parsed)) { - const typeValue = config.document.parsed[ENTITY_DISCRIMINATOR_NAME]; + } else if (isPlainObject(document.parsed)) { + const typeValue = document.parsed[ENTITY_DISCRIMINATOR_NAME]; if (typeof typeValue === 'string' && types[typeValue]) { rootType = types[typeValue]; } @@ -260,13 +254,13 @@ export async function lintEntityFile(opts: { const normalizedVisitors = normalizeVisitors(rules, types); const resolvedRefMap = await resolveDocument({ - rootDocument: config.document, + rootDocument: document, rootType, externalRefResolver, }); walkDocument({ - document: config.document, + document, rootType, normalizedVisitors, resolvedRefMap, From f4972a4a03dcd58d76f9fcb88efa3fcb111cfeb1 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 15:53:54 +0100 Subject: [PATCH 17/19] tests: remove unnecessary var --- .../core/src/rules/common/__tests__/entity-key-valid.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts index 4e6a63176a..d185a4a5b7 100644 --- a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts @@ -19,7 +19,6 @@ function lintEntityKey(source: string): WalkContext['problems'] { const ctx: WalkContext = { problems: [], specVersion: 'entity' as SpecVersion, // FIXME: this should be proper SpecVersion - config: {} as Config, visitorsData: {}, }; From 4b6116d5a30e0f67cedc3165f54ae97020e0f0db Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Wed, 29 Oct 2025 16:23:27 +0100 Subject: [PATCH 18/19] chore: add catalogEntity visitor types --- packages/core/src/lint.ts | 2 +- .../__tests__/entity-key-valid.test.ts | 0 .../src/rules/{common => catalogEntity}/entity-key-valid.ts | 4 ++-- packages/core/src/visitors.ts | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) rename packages/core/src/rules/{common => catalogEntity}/__tests__/entity-key-valid.test.ts (100%) rename packages/core/src/rules/{common => catalogEntity}/entity-key-valid.ts (91%) diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index 9a9322db5a..b913be506e 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -11,7 +11,7 @@ import { createConfigTypes } from './types/redocly-yaml.js'; import { createEntityTypes, ENTITY_DISCRIMINATOR_NAME } from './types/entity-yaml.js'; import { Struct } from './rules/common/struct.js'; import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js'; -import { EntityKeyValid } from './rules/common/entity-key-valid.js'; +import { EntityKeyValid } from './rules/catalogEntity/entity-key-valid.js'; import { type Config } from './config/index.js'; import { isPlainObject } from './utils/is-plain-object.js'; diff --git a/packages/core/src/rules/common/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/catalogEntity/__tests__/entity-key-valid.test.ts similarity index 100% rename from packages/core/src/rules/common/__tests__/entity-key-valid.test.ts rename to packages/core/src/rules/catalogEntity/__tests__/entity-key-valid.test.ts diff --git a/packages/core/src/rules/common/entity-key-valid.ts b/packages/core/src/rules/catalogEntity/entity-key-valid.ts similarity index 91% rename from packages/core/src/rules/common/entity-key-valid.ts rename to packages/core/src/rules/catalogEntity/entity-key-valid.ts index 6faaf649c0..039db7e988 100644 --- a/packages/core/src/rules/common/entity-key-valid.ts +++ b/packages/core/src/rules/catalogEntity/entity-key-valid.ts @@ -1,11 +1,11 @@ import type { UserContext } from '../../walk.js'; -import type { Oas3Rule } from '../../visitors.js'; +import type { CatalogEntityRule } from '../../visitors.js'; const validKeyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const MIN_KEY_LENGTH = 2; const MAX_KEY_LENGTH = 150; -export const EntityKeyValid: Oas3Rule = () => { +export const EntityKeyValid: CatalogEntityRule = () => { return { any(node: any, { report, location }: UserContext) { if (typeof node === 'object' && node !== null && 'key' in node) { diff --git a/packages/core/src/visitors.ts b/packages/core/src/visitors.ts index 1958872f3c..56b4f1a834 100644 --- a/packages/core/src/visitors.ts +++ b/packages/core/src/visitors.ts @@ -352,6 +352,8 @@ export type Overlay1Visitor = BaseVisitor & Overlay1NestedVisitor & Record | NestedVisitObject>; +export type CatalogEntityVisitor = BaseVisitor & Record>; + export type NestedVisitor = Exclude; export type NormalizedOasVisitors = { @@ -377,6 +379,9 @@ export type Async2Rule = (options: Record) => Async2Visitor | Async export type Async3Rule = (options: Record) => Async3Visitor | Async3Visitor[]; export type Arazzo1Rule = (options: Record) => Arazzo1Visitor | Arazzo1Visitor[]; export type Overlay1Rule = (options: Record) => Overlay1Visitor | Overlay1Visitor[]; +export type CatalogEntityRule = ( + options: Record +) => CatalogEntityVisitor | CatalogEntityVisitor[]; export type Oas3Preprocessor = (options: Record) => Oas3Visitor; export type Oas2Preprocessor = (options: Record) => Oas2Visitor; export type Async2Preprocessor = (options: Record) => Async2Visitor; From 830ee3c4fa9d6d28577d45137804aa8c528edc77 Mon Sep 17 00:00:00 2001 From: Kamil Ciula Date: Thu, 30 Oct 2025 08:18:17 +0100 Subject: [PATCH 19/19] chore: remove unnecessary imports/exports --- packages/core/src/__tests__/lint.test.ts | 1 - packages/core/src/index.ts | 1 - packages/core/src/lint.ts | 2 +- .../__tests__/entity-key-valid.test.ts | 1 - .../rules/{catalogEntity => catalog-entity}/entity-key-valid.ts | 0 5 files changed, 1 insertion(+), 4 deletions(-) rename packages/core/src/rules/{catalogEntity => catalog-entity}/__tests__/entity-key-valid.test.ts (98%) rename packages/core/src/rules/{catalogEntity => catalog-entity}/entity-key-valid.ts (100%) diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index cc05390f13..745e9d737b 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -15,7 +15,6 @@ import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import { lintEntityFile } from '../lint.js'; import { makeDocumentFromString } from '../resolve.js'; -import { type Config } from '../config/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6b5099428f..2f02034320 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -156,4 +156,3 @@ export type { ResolvedSecurity, } from './typings/arazzo.js'; export type { StatsAccumulator, StatsName } from './typings/common.js'; -export { getNodeTypesFromJSONSchema } from './types/json-schema-adapter.js'; diff --git a/packages/core/src/lint.ts b/packages/core/src/lint.ts index b913be506e..47ef7f9434 100755 --- a/packages/core/src/lint.ts +++ b/packages/core/src/lint.ts @@ -11,7 +11,7 @@ import { createConfigTypes } from './types/redocly-yaml.js'; import { createEntityTypes, ENTITY_DISCRIMINATOR_NAME } from './types/entity-yaml.js'; import { Struct } from './rules/common/struct.js'; import { NoUnresolvedRefs } from './rules/common/no-unresolved-refs.js'; -import { EntityKeyValid } from './rules/catalogEntity/entity-key-valid.js'; +import { EntityKeyValid } from './rules/catalog-entity/entity-key-valid.js'; import { type Config } from './config/index.js'; import { isPlainObject } from './utils/is-plain-object.js'; diff --git a/packages/core/src/rules/catalogEntity/__tests__/entity-key-valid.test.ts b/packages/core/src/rules/catalog-entity/__tests__/entity-key-valid.test.ts similarity index 98% rename from packages/core/src/rules/catalogEntity/__tests__/entity-key-valid.test.ts rename to packages/core/src/rules/catalog-entity/__tests__/entity-key-valid.test.ts index d185a4a5b7..231011beaa 100644 --- a/packages/core/src/rules/catalogEntity/__tests__/entity-key-valid.test.ts +++ b/packages/core/src/rules/catalog-entity/__tests__/entity-key-valid.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { makeDocumentFromString } from '../../../resolve.js'; -import { Config } from '../../../config/index.js'; import { createEntityTypes } from '../../../types/entity-yaml.js'; import { normalizeTypes } from '../../../types/index.js'; import { normalizeVisitors } from '../../../visitors.js'; diff --git a/packages/core/src/rules/catalogEntity/entity-key-valid.ts b/packages/core/src/rules/catalog-entity/entity-key-valid.ts similarity index 100% rename from packages/core/src/rules/catalogEntity/entity-key-valid.ts rename to packages/core/src/rules/catalog-entity/entity-key-valid.ts