Skip to content

Commit d6d8885

Browse files
committed
Add Context, StatusPage, and related endpoints
1 parent 45408be commit d6d8885

95 files changed

Lines changed: 4711 additions & 200 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/codegen/ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#!/usr/bin/env bash
22

3+
INFILE=${1:-"core.raml"}
34
SPECFILE=/tmp/api-specification-codegen-ts-$(date +%s).json
45

56
mkdir -p dist/codegen
67

7-
bin/openapi3 core.raml > $SPECFILE
8+
bin/openapi3 $INFILE > $SPECFILE
89

910
cd bin
1011

bin/docs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env bash
22

3-
bin/redoc core.raml > dist/index.html
3+
bin/redoc context.raml > dist/index.html

bin/lib/openapi3-post-processing.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ function normalizeOpenApi3(spec) {
7373
return normalizeLogo(removeAnnotations(normalizeSecuritySchemes(spec)))
7474
.replace(/x-amf-union/g, "anyOf")
7575
.replace(/x-core\.tags/g, "tags")
76+
.replace(/x-core\.badges/g, "x-badges")
7677
.replace(/x-core\.tagGroups/g, "x-tagGroups")
7778
.replace(/x-core\.expandable/g, "x-expandable")
78-
.replace(/("x-expandable":)null/g, "$1true")
79+
.replace(/x-core\.delegable/g, "x-delegable")
80+
.replace(/("x-(expandable|delegable)":)null/g, "$1true")
7981
.replace(/x-prelude\.discriminatorJunction/g, "discriminator")
8082
.replace(/,"anyOf":\[{"\$ref":"[^"]+"}]/g, "")
8183
// FIXME: the following shouldn't be necessary, but something causes the

bin/lib/post-processing.js

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,37 @@ function isolateCommonProperties(_resolved) {
232232
})
233233
}
234234

235+
function expandBadges(resolved, badges) {
236+
if (!badges) return
237+
238+
const badgeDefinitions = util.findCustomDomainProperty(resolved.encodes, "core.badges")?.extension?.members
239+
240+
if (!badgeDefinitions) return
241+
242+
const expanded = new amf.ArrayNode()
243+
244+
badges.extension.members.forEach(ref => {
245+
const name = ref.value.value(),
246+
defn = badgeDefinitions.find(m => m.properties.name.value.value() === name)
247+
248+
expanded.addMember(defn)
249+
})
250+
251+
badges.withExtension(expanded)
252+
253+
return badges
254+
}
255+
235256
function copyResourceTagsToMethods(resolved) {
236257
resolved.encodes.endPoints.forEach((endpoint) => {
237-
var tags = endpoint.customDomainProperties.find((prop) => prop.name.value() == "core.tags")
238-
if (!tags) return
258+
const tags = util.findCustomDomainProperty(endpoint, "core.tags"),
259+
badges = expandBadges(resolved, util.findCustomDomainProperty(endpoint, "core.badges"))
239260

240-
endpoint.operations
241-
.forEach((operation) => operation.withCustomDomainProperties([tags].concat(operation.customDomainProperties)))
261+
if (!tags && !badges) return
262+
263+
endpoint.operations.forEach((operation) =>
264+
operation.withCustomDomainProperties([tags, badges].filter(p => p).concat(operation.customDomainProperties))
265+
)
242266
})
243267
}
244268

@@ -300,25 +324,29 @@ function mergeResourceSpecBindIdsToResponses(resolved) {
300324
})
301325
}
302326

327+
function maybeMergeResourceAnnotationToResponse(response, annotation, resourceAnnotation) {
328+
const responseAnnotation = util.findCustomDomainProperty(response, annotation)
329+
330+
if (!responseAnnotation && resourceAnnotation) {
331+
response.withCustomDomainProperties([resourceAnnotation].concat(response.customDomainProperties))
332+
}
333+
}
334+
303335
function mergeResourceSpecFactoryAnnotationsToResponses(resolved) {
304336
resolved.encodes.endPoints.forEach((endPoint) => {
305337
var factoryStrategy = util.findCustomDomainProperty(endPoint, "specs.factoryStrategy"),
306-
factoryName = util.findCustomDomainProperty(endPoint, "specs.factoryName")
338+
factoryName = util.findCustomDomainProperty(endPoint, "specs.factoryName"),
339+
contextualRequestBodies = util.findCustomDomainProperty(endPoint, "specs.contextualRequestBodies"),
340+
baseContext = util.findCustomDomainProperty(endPoint, "specs.baseContext")
307341

308-
if (!factoryStrategy && !factoryName) return
342+
if (!factoryStrategy && !factoryName && !contextualRequestBodies && !baseContext) return
309343

310344
endPoint.operations.forEach((operation) => {
311345
operation.responses.forEach((response) => {
312-
let rStrategy = util.findCustomDomainProperty(response, "specs.factoryStrategy"),
313-
rName = util.findCustomDomainProperty(response, "specs.factoryName")
314-
315-
if (!rStrategy && factoryStrategy) {
316-
response.withCustomDomainProperties([factoryStrategy].concat(response.customDomainProperties))
317-
}
318-
319-
if (!rName && factoryName) {
320-
response.withCustomDomainProperties([factoryName].concat(response.customDomainProperties))
321-
}
346+
maybeMergeResourceAnnotationToResponse(response, "specs.factoryStrategy", factoryStrategy)
347+
maybeMergeResourceAnnotationToResponse(response, "specs.factoryName", factoryName)
348+
maybeMergeResourceAnnotationToResponse(response, "specs.contextualRequestBodies", contextualRequestBodies)
349+
maybeMergeResourceAnnotationToResponse(response, "specs.baseContext", baseContext)
322350
})
323351
})
324352
})
@@ -376,6 +404,29 @@ function moveFieldDescriptionsToProperties(resolved) {
376404
})
377405
}
378406

407+
function applyDelegableFieldFlags(_resolved) {
408+
Object.values(hoistedShapes).forEach(shape => {
409+
const delegableFields = util.findCustomDomainProperty(shape, "core.delegableFields")
410+
411+
if (delegableFields) {
412+
delegableFields.extension.members.forEach(field => {
413+
const fieldName = field.value.value(),
414+
property = shape.properties.find(p => p.name == fieldName)
415+
416+
const flag = new amf.DomainExtension()
417+
flag.withName("core.delegable")
418+
flag.withExtension(new amf.ScalarNode("", "http://www.w3.org/2001/XMLSchema#nil"))
419+
420+
property.range.withCustomDomainProperties([flag])
421+
})
422+
423+
shape.withCustomDomainProperties(shape.customDomainProperties
424+
.filter((prop) => prop.name.value() !== "core.delegableFields")
425+
)
426+
}
427+
})
428+
}
429+
379430
function copyObjectDescriptionsToTags(resolved) {
380431
const tags = util.findCustomDomainProperty(resolved.encodes, "core.tags")
381432
if (!tags) return
@@ -391,13 +442,36 @@ function copyObjectDescriptionsToTags(resolved) {
391442
})
392443
}
393444

394-
exports.postProcess = (resolved) => {
445+
function extendAnnotation(name, resolved, original) {
446+
debugger
447+
const ext = util.findCustomDomainProperty(resolved.encodes, name),
448+
orig = util.findCustomDomainProperty(original.encodes, name),
449+
extension = new amf.ArrayNode()
450+
451+
orig.extension.members.forEach(m => extension.addMember(m))
452+
ext.extension.members.forEach(m => extension.addMember(m))
453+
454+
ext.withExtension(extension)
455+
}
456+
457+
function extendTagAnnotations(resolved, parsed) {
458+
if (!parsed) return
459+
460+
const original = parsed.references().find(r => r.encodes?.id?.endsWith("/web-api"))
461+
462+
extendAnnotation("core.tags", resolved, original)
463+
extendAnnotation("core.tagGroups", resolved, original)
464+
}
465+
466+
exports.postProcess = (resolved, parsed) => {
467+
extendTagAnnotations(resolved, parsed)
395468
hoistPayloadExamples(resolved)
396469
hoistCommonTypes(resolved)
397470
isolateCommonProperties(resolved)
398471
hoistDiscriminatorJunctions(resolved)
399472
copyResourceTagsToMethods(resolved)
400473
moveFieldDescriptionsToProperties(resolved)
474+
applyDelegableFieldFlags(resolved)
401475
mergeResourceSpecBindIdsToResponses(resolved)
402476
mergeResourceSpecFactoryAnnotationsToResponses(resolved)
403477
copyObjectDescriptionsToTags(resolved)

bin/lib/request-spec-generator.js

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ const amf = require("amf-client-js")
22
const util = require("./utilities")
33

44
class RequestSpecGenerator {
5-
constructor(resolved) {
5+
constructor(resolved, tags, target) {
6+
this.tags = tags
7+
this.target = target || "state"
68
this.resolved = resolved
79
this.schemas = {}
810
this.spec = undefined
@@ -18,7 +20,7 @@ class RequestSpecGenerator {
1820
requestBindings(operation, response, requestBodyInfo) {
1921
if (!this.supportsRequestBody(operation)) return []
2022

21-
var paramsBinding, headersBinding = 'headers.merge! "content-type" => "application/json"'
23+
var paramsBinding, headersBinding = 'headers["content-type"] = "application/json"'
2224

2325
var requestBody = response.customDomainProperties.find(d => d.name.value() == "specs.requestBody")?.extension?.value?.value(),
2426
attributesStrategy = this.factoryAttributesStrategy(response),
@@ -62,7 +64,7 @@ class RequestSpecGenerator {
6264

6365
permissionLevel = permissionLevel.extension.value.value()
6466

65-
return `let(:api_key) { create :api_key, :${permissionLevel}, user: api_key_user }`
67+
return `let(:api_key) { op_create :api_key, :${permissionLevel}, params: { user: api_key_user.id.to_s }${this.bindingContext()} }`
6668
}
6769

6870
idBindings(response) {
@@ -78,7 +80,7 @@ class RequestSpecGenerator {
7880
if (idValue == "auto" || idValue.startsWith("auto:")) {
7981
var traits = idValue.split(":").slice(1),
8082
bindTraits = traits.length == 0 ? "" : `, ${traits.map(t => ":" + t).join(", ")}`
81-
bindValue = `${factoryStrategy}(:${factoryName || model}${bindTraits}).id.to_s`
83+
bindValue = `${factoryStrategy}(:${factoryName || model}${bindTraits}${this.bindingContext()}).id.to_s`
8284
} else {
8385
bindValue = `"${idValue}"`
8486
}
@@ -114,6 +116,7 @@ class RequestSpecGenerator {
114116
return `
115117
it "${this.testCaseDescription(requestBodyInfo)}"${this.testCaseFlags(requestBodyInfo)} do
116118
${bindings.join("\n")}
119+
${this.authorizationHeaders(response)}
117120
${operation.method.value()} "${this.basePath}${this.endPointPath(endPoint, idBindings)}"${this.requestOptions(operation, bindings[0])}
118121
119122
expect(response).to have_http_status(${code})
@@ -152,6 +155,43 @@ class RequestSpecGenerator {
152155
}).join("\n\n");
153156
}
154157

158+
baseContextBinding(response) {
159+
var baseCtx = util.findCustomDomainProperty(response, "specs.baseContext")
160+
161+
if (!baseCtx) return ""
162+
163+
return `let(:context) { ${baseCtx.extension.value.value()} }`
164+
}
165+
166+
contextualResponseTestCases(endPoint, operation, response, code, idBindings, bodyExpectation) {
167+
var ctxs = util.findCustomDomainProperty(response, "specs.contextualRequestBodies")?.extension?.members
168+
if (!ctxs) return ""
169+
170+
return ctxs.map(ctxMember => {
171+
const ctx = ctxMember.value.value()
172+
173+
return `
174+
context "when the context is a ${ctx.replace("@", "")}" do
175+
let(:context) { ${ctx} }
176+
177+
${this.responseTestCase(endPoint, operation, response, code, idBindings, bodyExpectation)}
178+
end
179+
`
180+
}).join("\n\n")
181+
}
182+
183+
authorizationHeaders(response) {
184+
const authStrategy = util.findCustomDomainProperty(response, "specs.authStrategy")?.extension?.value?.value()
185+
186+
if (authStrategy === "session") {
187+
return `
188+
headers["authorization"] = "Session"
189+
headers["cookie"] = "session_token=#{api_key_user.remember_token}"
190+
headers["sec-fetch-site"] = "same-origin"
191+
`
192+
} else return ""
193+
}
194+
155195
beforeHook(response) {
156196
var hook = util.findCustomDomainProperty(response, "specs.beforeHook")
157197

@@ -170,13 +210,15 @@ class RequestSpecGenerator {
170210
var factoryName = this.factoryName(response);
171211

172212
return Object.entries(createModels.extension.properties).map(([model, options]) => {
173-
var count = options.properties.count?.value?.value() || 3,
213+
var count = options.properties.count?.value?.value() ?? 3,
174214
traits = options.properties.traits?.value?.value()?.split(":") || [],
175215
traitsSection = traits.length == 0 ? "" : `, ${traits.map(t => ":" + t).join(", ")}`,
176216
factoryOptions = Object.entries(options.properties.options?.properties || {}),
177217
factoryOptionsSection = factoryOptions.length == 0 ? "" : `, ${factoryOptions.map(([opt, val]) => `${opt}: ${val.value.value()}`).join(", ")}`
178218

179-
return `before { ${strategy}_list :${factoryName || model}, ${count}${traitsSection}${factoryOptionsSection} }`
219+
if (count <= 0) return ""
220+
221+
return `before { ${strategy}_list :${factoryName || model}, ${count}${traitsSection}${factoryOptionsSection}${this.bindingContext()} }`
180222
}).join("\n")
181223
}
182224

@@ -202,24 +244,28 @@ class RequestSpecGenerator {
202244
context "${code} (${response.description.value().trim().replace(/\.$/, "")})" do
203245
${this.apiKeyBinding(response)}
204246
${idBindings}
247+
${this.baseContextBinding(response)}
205248
206249
${this.beforeHook(response)}
207250
${this.createModelsHook(response)}
208251
209252
${this.responseTestCase(endPoint, operation, response, code, idBindings, bodyExpectation)}
210253
${this.exhaustiveResponseTestCases(endPoint, operation, response, code, idBindings, bodyExpectation)}
254+
${this.contextualResponseTestCases(endPoint, operation, response, code, idBindings, bodyExpectation)}
211255
end
212256
`
213257
}, "").join("\n\n")
214258
}
215259

216260
operations() {
217261
return this.resolved.encodes.endPoints.map((endPoint) => {
218-
const resourceTag = util.findCustomDomainProperty(endPoint, "core.tags")?.extension?.members?.[0]?.value?.value() || ""
262+
const resourceTags = util.findCustomDomainProperty(endPoint, "core.tags")?.extension?.members || []
263+
264+
if (!this.isTagEnabled(resourceTags)) return ""
219265

220266
return endPoint.operations.map((operation) => {
221267
return `
222-
describe "${operation.method.value().toUpperCase()} ${this.basePath}${endPoint.path.value()}"${resourceTag && `, resource: "${resourceTag}"`} do
268+
describe "${operation.method.value().toUpperCase()} ${this.basePath}${endPoint.path.value()}"${(resourceTags[0] ?? "") && `, resource: "${resourceTags[0].value.value()}"`} do
223269
${this.responses(endPoint, operation)}
224270
end
225271
`
@@ -232,16 +278,44 @@ class RequestSpecGenerator {
232278
require "rails_helper"
233279
234280
RSpec.describe "${this.basePath} (version: latest)", :timeline_simulation do
235-
before(:example) { DatabaseCleaner.clean }
236-
let(:api_key_user) { create :user, :admin }
237-
let(:api_key) { create :api_key, :write, user: api_key_user }
281+
before(${this.target === "website" ? ":context" : ":example"}) { DatabaseCleaner.clean }
282+
${this.rootBeforeHook()}
283+
284+
let(:api_key_user) { op_create :user, :admin${this.bindingContext()} }
285+
let(:api_key) { op_create :api_key, :write, params: { user: api_key_user.id.to_s }${this.bindingContext()} }
286+
238287
let(:headers) { { accept: "application/hal+json;variant=compact", authorization: "Bearer #{api_key.token}" } }
239288
289+
${this.rootContextBindings()}
290+
240291
${content}
241292
end
242293
`
243294
}
244295

296+
rootBeforeHook() {
297+
if (this.target !== "website") return ""
298+
299+
return `
300+
before(:context) do
301+
@client = VCR.use_cassette("op_create/client") { op_create :client }
302+
@primary_status_page = @client.status_pages.first
303+
@secondary_status_page = op_create :status_page, client: @client
304+
end
305+
`
306+
}
307+
308+
rootContextBindings() {
309+
if (this.target !== "website") return ""
310+
311+
return `
312+
let(:context) { @primary_status_page }
313+
let(:context_host) { "#{context.domain}.hund.localhost" }
314+
315+
before { host! context_host }
316+
`
317+
}
318+
245319
schemaId(schema) {
246320
return schema.id
247321
.split("#")[1]
@@ -250,6 +324,18 @@ class RequestSpecGenerator {
250324
.replace(/%/g, "_")
251325
}
252326

327+
isTagEnabled(tags) {
328+
if (!this.tags) return true
329+
330+
return tags.some(tag => this.tags.includes(tag.value.value()))
331+
}
332+
333+
bindingContext() {
334+
if (this.target === "website") return ", client: @client"
335+
336+
return ""
337+
}
338+
253339
supportsRequestBody(operation) {
254340
return ["post", "put", "patch", "delete"].includes(operation.method.value())
255341
}

0 commit comments

Comments
 (0)