diff --git a/demo/apis.json b/demo/apis.json
index ac9b1eb..bdde942 100644
--- a/demo/apis.json
+++ b/demo/apis.json
@@ -21,5 +21,6 @@
"agents-api/agents-api.yaml": { "type": "OAS 3.0", "mime": "application/yaml" },
"tags-flights/tags-flights.yml": { "type": "OAS 3.0", "mime": "application/yaml" },
"oas32-query/oas32-query.yaml": { "type": "OAS 3.2", "mime": "application/yaml" },
+ "oas31-webhooks/oas31-webhooks.yaml": { "type": "OAS 3.1", "mime": "application/yaml" },
"grpc-test.json": { "type": "gRPC", "mime": "application/json" }
}
diff --git a/demo/index.js b/demo/index.js
index aef01ca..45d6f8e 100644
--- a/demo/index.js
+++ b/demo/index.js
@@ -31,6 +31,7 @@ class ApiDemo extends ApiDemoPage {
["APIC-641", "APIC-641"],
["W-10881270", "W-10881270"],
["oas32-query", "OAS 3.2 (QUERY)"],
+ ["oas31-webhooks", "OAS 3.1 (Webhooks)"],
].map(
([file, label]) => html`
${label}
diff --git a/demo/oas31-webhooks/oas31-webhooks.yaml b/demo/oas31-webhooks/oas31-webhooks.yaml
new file mode 100644
index 0000000..8052546
--- /dev/null
+++ b/demo/oas31-webhooks/oas31-webhooks.yaml
@@ -0,0 +1,136 @@
+openapi: 3.1.0
+info:
+ title: Webhooks + Schema Composition Spike
+ version: 1.0.0
+paths:
+ /pets:
+ get:
+ operationId: listPets
+ summary: List pets
+ responses:
+ "200":
+ description: A list of pets
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Pet"
+
+# Top-level `webhooks` map of Path Item Objects, confirmed in OAS 3.1.0 spec
+# (versions/3.1.0.md, field `webhooks`, sibling to `paths`/`components`).
+webhooks:
+ newPet:
+ post:
+ operationId: newPetWebhook
+ summary: Notify subscriber of a newly created pet
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Pet"
+ responses:
+ "200":
+ description: Webhook received successfully
+
+components:
+ schemas:
+ Pet:
+ # allOf: base fields + discriminated extra fields
+ allOf:
+ - $ref: "#/components/schemas/PetBase"
+ - $ref: "#/components/schemas/PetKindFields"
+
+ PetBase:
+ type: object
+ required: [id, name]
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ kind:
+ type: string
+ enum: [dog, cat]
+
+ # if/then/else conditional (JSON Schema 2020-12, dialect used by OAS 3.1)
+ PetKindFields:
+ if:
+ properties:
+ kind:
+ const: dog
+ then:
+ properties:
+ breed:
+ type: string
+ required: [breed]
+ else:
+ properties:
+ indoor:
+ type: boolean
+ required: [indoor]
+
+ # oneOf: mutually exclusive contact methods
+ ContactMethod:
+ oneOf:
+ - $ref: "#/components/schemas/EmailContact"
+ - $ref: "#/components/schemas/PhoneContact"
+
+ EmailContact:
+ type: object
+ required: [email]
+ properties:
+ email:
+ type: string
+ format: email
+
+ PhoneContact:
+ type: object
+ required: [phone]
+ properties:
+ phone:
+ type: string
+
+ # anyOf: owner may have one or more identifiers present
+ OwnerIdentifiers:
+ anyOf:
+ - required: [ownerId]
+ properties:
+ ownerId:
+ type: integer
+ - required: [ownerEmail]
+ properties:
+ ownerEmail:
+ type: string
+ format: email
+
+ # oneOf with inline anonymous object members (no $ref). AMF assigns synthetic
+ # names (item0/item1); the component must render positional "Option N" labels.
+ InlineChoice:
+ oneOf:
+ - type: object
+ properties:
+ a:
+ type: string
+ - type: object
+ properties:
+ b:
+ type: integer
+
+ # oneOf whose first member is an inline anonymous scalar. The synthetic name
+ # must be replaced by the datatype label ("String").
+ ScalarChoice:
+ oneOf:
+ - type: string
+ - type: integer
+
+ # allOf mixing a named $ref (PetBase, preserved) with an inline anonymous
+ # member (synthetic name -> label nulled).
+ MixedAllOf:
+ allOf:
+ - $ref: "#/components/schemas/PetBase"
+ - type: object
+ properties:
+ extra:
+ type: string
diff --git a/package-lock.json b/package-lock.json
index b2638ff..70f02d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,18 @@
{
"name": "@api-components/api-summary",
- "version": "4.6.20",
+ "version": "4.6.21",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@api-components/api-summary",
- "version": "4.6.20",
+ "version": "4.6.21",
"license": "Apache-2.0",
"dependencies": {
"@advanced-rest-client/arc-marked": "^1.1.2",
"@advanced-rest-client/icons": "^4.0.2",
"@advanced-rest-client/markdown-styles": "^3.1.5",
- "@api-components/amf-helper-mixin": "^4.5.36",
+ "@api-components/amf-helper-mixin": "^4.5.38",
"@api-components/api-method-documentation": "^5.2.5",
"@api-components/api-model-generator": "^0.4.0",
"@api-components/http-method-label": "^3.1.4",
diff --git a/package.json b/package.json
index 81c026f..abfd257 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@api-components/api-summary",
"description": "A summary view for an API base on AMF data model",
- "version": "4.6.20",
+ "version": "4.6.21",
"license": "Apache-2.0",
"main": "index.js",
"module": "index.js",
@@ -28,7 +28,7 @@
"@advanced-rest-client/arc-marked": "^1.1.2",
"@advanced-rest-client/icons": "^4.0.2",
"@advanced-rest-client/markdown-styles": "^3.1.5",
- "@api-components/amf-helper-mixin": "^4.5.36",
+ "@api-components/amf-helper-mixin": "^4.5.38",
"@api-components/api-method-documentation": "^5.2.5",
"@api-components/api-model-generator": "^0.4.0",
"@api-components/http-method-label": "^3.1.4",
diff --git a/src/ApiSummary.d.ts b/src/ApiSummary.d.ts
index 480ec51..43cebb1 100644
--- a/src/ApiSummary.d.ts
+++ b/src/ApiSummary.d.ts
@@ -35,6 +35,7 @@ export declare class ApiSummary extends AmfHelperMixin(LitElement) {
_licenseName: string;
_licenseUrl: string;
_endpoints: any[];
+ _webhooks: any[];
_termsOfService: string;
_version: string;
_apiTitle: string;
@@ -96,6 +97,12 @@ export declare class ApiSummary extends AmfHelperMixin(LitElement) {
*/
_computeEndpoints(webApi: any): any[]|undefined;
+ /**
+ * Computes view model for the webhooks list (OAS 3.1/3.2 top-level webhooks).
+ * @param webApi Web API model
+ */
+ _computeWebhooks(webApi: any): any[]|undefined;
+
/**
* Computes a view model for supported operations for an endpoint.
* @param {any} endpoint Endpoint model.
@@ -142,6 +149,8 @@ export declare class ApiSummary extends AmfHelperMixin(LitElement) {
_endpointsTemplate(): TemplateResult|string;
+ _webhooksTemplate(): TemplateResult|string;
+
_endpointTemplate(item): TemplateResult|string;
_endpointPathTemplate(item): TemplateResult;
diff --git a/src/ApiSummary.js b/src/ApiSummary.js
index e362c8d..6d4c431 100644
--- a/src/ApiSummary.js
+++ b/src/ApiSummary.js
@@ -52,6 +52,7 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
_licenseName: { type: String },
_licenseUrl: { type: String },
_endpoints: { type: Array },
+ _webhooks: { type: Array },
_termsOfService: { type: String },
_version: { type: String },
_apiTitle: { type: String },
@@ -134,6 +135,7 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
this._version = undefined;
this._termsOfService = undefined;
this._endpoints = undefined;
+ this._webhooks = undefined;
this._providerName = undefined;
this._providerEmail = undefined;
@@ -149,6 +151,7 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
this._version = this._computeVersion(webApi);
this._termsOfService = this._computeToS(webApi);
this._endpoints = this._computeEndpoints(webApi);
+ this._webhooks = this._computeWebhooks(webApi);
const provider = this._computeProvider(webApi);
this._providerName = this._computeName(provider);
@@ -318,6 +321,45 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
});
}
+ /**
+ * Computes view model for the webhooks list (OAS 3.1/3.2 top-level webhooks).
+ * Webhooks are `apiContract#EndPoint` nodes — the same shape as endpoints — so
+ * they reuse the endpoint item view model and `_endpointTemplate` for rendering.
+ * @param {any} webApi Web API model
+ * @return {any[]|undefined}
+ */
+ _computeWebhooks(webApi) {
+ if (!webApi) {
+ return undefined;
+ }
+ const key = this._getAmfKey(this.ns.aml.vocabularies.apiContract.webhooks);
+ const webhooks = this._ensureArray(webApi[key]);
+ if (!webhooks || !webhooks.length) {
+ return undefined;
+ }
+ return webhooks.map((item) => {
+ const path = this._getValue(
+ item,
+ this.ns.aml.vocabularies.apiContract.path
+ );
+ const name = this._getValue(item, this.ns.aml.vocabularies.core.name);
+ const pathStr = typeof path === "string" ? path : `/${name || ""}`;
+ const supportedOperations = this._endpointOperations(item);
+ const webhookDisplayInfo = this._computeEndpointName(
+ item,
+ webApi,
+ supportedOperations
+ );
+ return {
+ name: webhookDisplayInfo?.name,
+ description: webhookDisplayInfo?.description,
+ path: pathStr,
+ id: item["@id"],
+ ops: supportedOperations,
+ };
+ });
+ }
+
/**
* Computes a descriptive name for an endpoint based on its operations and tags.
* @param {any} endpoint Endpoint model
@@ -637,7 +679,8 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
${this._licenseTemplate()} ${this._termsOfServiceTemplate()}
- ${this.hideToc ? "" : this._endpointsTemplate()} `;
+ ${this.hideToc ? "" : this._endpointsTemplate()}
+ ${this.hideToc ? "" : this._webhooksTemplate()} `;
}
_titleTemplate() {
@@ -879,6 +922,26 @@ export class ApiSummary extends AmfHelperMixin(LitElement) {
`;
}
+ /**
+ * @return {TemplateResult|string} A template for the top-level webhooks list
+ * (OAS 3.1/3.2). Webhooks share the endpoint item shape, so each renders with
+ * the same `_endpointTemplate`.
+ */
+ _webhooksTemplate() {
+ const { _webhooks } = this;
+ if (!_webhooks || !_webhooks.length) {
+ return "";
+ }
+ const result = _webhooks.map((item) => this._endpointTemplate(item));
+ return html`
+
+
+
+ ${result}
+
+ `;
+ }
+
_endpointTemplate(item) {
const ops =
item.ops && item.ops.length
diff --git a/src/Styles.js b/src/Styles.js
index 2858b3e..4ad181a 100644
--- a/src/Styles.js
+++ b/src/Styles.js
@@ -209,7 +209,8 @@ export default css`
font-size: var(--api-summary-section-title-font-size);
}
- .section.endpoints-title {
+ .section.endpoints-title,
+ .section.webhooks-title {
font-weight: var(--arc-font-title-font-weight, 500);
color: var(--arc-font-title-color);
font-weight: var(--arc-font-title-font-weight);
diff --git a/test/api-summary.test.js b/test/api-summary.test.js
index a04b2d5..d91ff51 100644
--- a/test/api-summary.test.js
+++ b/test/api-summary.test.js
@@ -369,6 +369,64 @@ describe('ApiSummary', () => {
});
});
+ describe('Webhooks rendering (OAS 3.1)', () => {
+ let element = /** @type ApiSummary */ (null);
+ let amf;
+
+ before(async () => {
+ amf = await AmfLoader.load(compact, 'oas31-webhooks');
+ });
+
+ beforeEach(async () => {
+ element = await basicFixture();
+ element.amf = amf;
+ await aTimeout(0);
+ });
+
+ function webhooksToc() {
+ const titleNode = element.shadowRoot.querySelector('.webhooks-title');
+ return titleNode ? titleNode.closest('.toc') : null;
+ }
+
+ it('computes the webhooks view model', () => {
+ assert.typeOf(element._webhooks, 'array');
+ assert.lengthOf(element._webhooks, 1);
+ });
+
+ it('renders a Webhooks section title', () => {
+ const node = element.shadowRoot.querySelector('.webhooks-title');
+ assert.dom.equal(node, ``);
+ });
+
+ it('renders the webhook as an endpoint item', () => {
+ const nodes = webhooksToc().querySelectorAll('.endpoint-item');
+ assert.lengthOf(nodes, 1);
+ });
+
+ it('renders the webhook operation method', () => {
+ const node = webhooksToc().querySelector('.method-label');
+ assert.equal(node.getAttribute('data-method'), 'post');
+ assert.equal(node.getAttribute('data-shape-type'), 'method');
+ });
+
+ it('keeps endpoints and webhooks as separate sections', () => {
+ assert.ok(element.shadowRoot.querySelector('.endpoints-title'), 'has endpoints section');
+ assert.ok(element.shadowRoot.querySelector('.webhooks-title'), 'has webhooks section');
+ });
+
+ it('dispatches a navigation event when a webhook method is clicked', (done) => {
+ const node = webhooksToc().querySelector('.method-label[data-id]');
+ element.addEventListener('api-navigation-selection-changed', (e) => {
+ // @ts-ignore
+ const { detail } = e;
+ assert.typeOf(detail.selected, 'string');
+ assert.equal(detail.type, 'method');
+ done();
+ });
+ /** @type HTMLElement */ (node).click();
+ });
+ });
+
describe('Server rendering', () => {
let ramlSingleServerAmf;
let oasMultipleServersAmf;
@@ -494,6 +552,7 @@ describe('ApiSummary', () => {
assert.isUndefined(element._version);
assert.isUndefined(element._termsOfService);
assert.isUndefined(element._endpoints);
+ assert.isUndefined(element._webhooks);
});
});
});