Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion aep/0004.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ rules:
functionOptions:
schema:
type: object
required: [singular, plural]
required: [singular, plural, patterns]
properties:
type:
type: string
singular:
type: string
pattern: '^[a-z][a-z0-9-]*$'
Expand All @@ -21,8 +23,10 @@ rules:
pattern: '^[a-z][a-z0-9-]*$'
patterns:
type: array
minItems: 1
items:
type: string
pattern: '^[a-z][a-z0-9_-]*(/([a-z][a-z0-9_-]*|\{[a-z][a-z0-9_-]*\}))*$'
parents:
type: array
items:
Expand Down
40 changes: 34 additions & 6 deletions docs/0004.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ to define resource types in OpenAPI specifications.
## aep-0004-x-aep-resource-structure

**Rule**: x-aep-resource must have correct structure with required fields
(singular, plural)
(singular, plural, patterns)

This rule enforces that all `x-aep-resource` extensions have the required
fields and proper format, as mandated in [AEP-4].
Expand All @@ -23,10 +23,14 @@ that:

- `singular` - Must be in kebab-case (e.g., `book`, `book-edition`)
- `plural` - Must be in kebab-case (e.g., `books`, `book-editions`)
- `patterns` - Array of path patterns for the resource (minimum 1 pattern).
Each pattern must follow the format: `collection/{identifier}` with optional
nested resources

**Optional fields**:

- `patterns` - Array of path patterns for the resource
- `type` - Resource type in format `{API Name}/{Type Name}` (e.g.,
`library.example.com/book`)
- `parents` - Array of parent resource identifiers
- `singleton` - Boolean indicating if resource is a singleton

Expand Down Expand Up @@ -57,12 +61,32 @@ components:
x-aep-resource:
singular: Author # Should be lowercase kebab-case
plural: authors
patterns:
- 'authors/{author_id}'

Magazine:
type: object
x-aep-resource:
singular: magazine
plural: Magazines # Should be lowercase kebab-case
patterns:
- 'magazines/{magazine_id}'

Library:
type: object
x-aep-resource:
singular: library
plural: libraries
# Missing 'patterns' field (required)

Article:
type: object
x-aep-resource:
singular: article
plural: articles
patterns:
- 'invalid-pattern' # Invalid - must include path parameter
- 'Articles/{article_id}' # Invalid - should be lowercase
```

**Correct** code for this rule:
Expand All @@ -75,6 +99,8 @@ components:
x-aep-resource:
singular: book
plural: books
patterns:
- 'books/{book_id}'

BookEdition:
type: object
Expand Down Expand Up @@ -131,10 +157,12 @@ protobuf-specific and rely on `google.api.resource` annotations. This OpenAPI
rule validates the equivalent `x-aep-resource` extension structure as defined
in the AEP-4 specification.

**Pattern Validation**: This rule validates the presence and type of the
`patterns` field but does not validate the pattern format (e.g., alternating
collection/id segments). Pattern format validation may be added in a future
release.
**Pattern Validation**: This rule validates that the `patterns` field is
required and that each pattern follows the correct format:
`collection/{identifier}` with optional nested resources (e.g.,
`publishers/{publisher_id}/books/{book_id}`). Patterns must start with a
lowercase letter, use kebab-case for collection names, and include path
parameters in braces.

**Pattern Uniqueness**: This rule does not validate that patterns do not
overlap in the set of resource paths they can match, as required in AEP-4 spec
Expand Down
1 change: 1 addition & 0 deletions test/0004/backward-compat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ test('aep-0004-x-aep-resource-structure should validate mixed scenarios', () =>
'x-aep-resource': {
singular: 'book',
plural: 'books',
patterns: ['books/{book_id}'],
},
},
Publisher: {
Expand Down
105 changes: 105 additions & 0 deletions test/0004/structure.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,23 @@ test('aep-0004-x-aep-resource-structure should find errors for invalid field for
'x-aep-resource': {
singular: 'Author', // should be kebab-case lowercase
plural: 'authors',
patterns: ['authors/{author_id}'],
},
},
Magazine: {
type: 'object',
'x-aep-resource': {
singular: 'magazine',
plural: 'Magazines', // should be kebab-case lowercase
patterns: ['magazines/{magazine_id}'],
},
},
BookStore: {
type: 'object',
'x-aep-resource': {
singular: 'book_store', // should use hyphens not underscores
plural: 'book-stores',
patterns: ['book-stores/{book_store_id}'],
},
},
},
Expand Down Expand Up @@ -95,6 +98,7 @@ test('aep-0004-x-aep-resource-structure should find no errors for valid minimal
'x-aep-resource': {
singular: 'book',
plural: 'books',
patterns: ['books/{book_id}'],
},
},
},
Expand Down Expand Up @@ -154,6 +158,107 @@ test('aep-0004-x-aep-resource-structure should allow kebab-case in singular and
'x-aep-resource': {
singular: 'book-edition',
plural: 'book-editions',
patterns: ['book-editions/{book-edition_id}'],
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(0);
});
});

test('aep-0004-x-aep-resource-structure should find error for missing patterns field', () => {
const oasDoc = {
openapi: '3.0.3',
components: {
schemas: {
Book: {
type: 'object',
'x-aep-resource': {
singular: 'book',
plural: 'books',
// missing 'patterns' field
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(1);
expect(results).toContainMatch({
path: ['components', 'schemas', 'Book', 'x-aep-resource'],
message: 'The x-aep-resource extension does not conform to AEP-4 requirements',
});
});
});

test('aep-0004-x-aep-resource-structure should find error for empty patterns array', () => {
const oasDoc = {
openapi: '3.0.3',
components: {
schemas: {
Book: {
type: 'object',
'x-aep-resource': {
singular: 'book',
plural: 'books',
patterns: [], // empty array
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBe(1);
expect(results).toContainMatch({
path: ['components', 'schemas', 'Book', 'x-aep-resource', 'patterns'],
});
});
});

test('aep-0004-x-aep-resource-structure should find error for invalid pattern format', () => {
const oasDoc = {
openapi: '3.0.3',
components: {
schemas: {
Book: {
type: 'object',
'x-aep-resource': {
singular: 'book',
plural: 'books',
patterns: [
'books/{book_id}', // valid
'Books/{book_id}', // invalid - starts with uppercase
'books/{Book_id}', // invalid - parameter has uppercase
'books/{book-id}/chapters/{chapter_id}/pages/{Page_id}', // invalid - parameter has uppercase
],
},
},
},
},
};
return linter.run(oasDoc).then((results) => {
expect(results.length).toBeGreaterThan(0);
expect(results).toContainMatch({
path: ['components', 'schemas', 'Book', 'x-aep-resource', 'patterns', '1'],
});
});
});

test('aep-0004-x-aep-resource-structure should allow optional type field', () => {
const oasDoc = {
openapi: '3.0.3',
components: {
schemas: {
Book: {
type: 'object',
'x-aep-resource': {
type: 'library.example.com/book',
singular: 'book',
plural: 'books',
patterns: ['books/{book_id}'],
},
},
},
Expand Down