@voxgig/apidef 8.3.0 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/model/guide.aon CHANGED
@@ -3,6 +3,48 @@ guide: entity: &: {
3
3
  active?: boolean
4
4
  name: key()
5
5
 
6
+ # COMPOSITE IDENTITY OVERRIDE.
7
+ #
8
+ # Some APIs address one record by several path parameters at once, with no
9
+ # single parameter that is the id: github needs {owner} and {repo} together
10
+ # to name a repository, and cloudsmith needs {owner}/{repo}/{identifier}
11
+ # for an entitlement. The model carries those as `id.parts`, joined by
12
+ # `id.sep` into the one id an SDK entity holds.
13
+ #
14
+ # apidef infers this from ADJACENT variable path segments, which is right
15
+ # far more often than not but cannot always be right: a trailing variable
16
+ # is sometimes a MODIFIER rather than part of the key. github's
17
+ # `/…/artifacts/{artifact_id}/{archive_format}` is the clear case —
18
+ # `archive_format` selects zip or tar, it does not identify the artifact —
19
+ # and no reading of the path alone distinguishes that from a genuine
20
+ # compound key.
21
+ #
22
+ # So state it here when the inference is wrong:
23
+ #
24
+ # id: parts: [ 'owner', 'repo' ] # this IS the compound key
25
+ # id: composite: false # it is not; the last param is the id
26
+ #
27
+ # An explicit `parts` wins over the inference. `composite: false` turns the
28
+ # inference off, which is what `archive_format` above wants.
29
+ #
30
+ # THE FLAG IS A BOOLEAN, NOT AN EMPTY `parts`. Writing `parts: []` reads as
31
+ # "no parts" but cannot work: aontu resolves an empty list to nothing, so
32
+ # the key arrives absent and is indistinguishable from never having been
33
+ # set — the inference then runs anyway. That was tried first.
34
+ #
35
+ # `sep` defaults to '/' and rarely wants changing: a slash cannot occur
36
+ # inside one path segment, so the join is unambiguous.
37
+ id?: {
38
+ parts?: [ &: string ]
39
+ sep?: string
40
+ composite?: boolean
41
+
42
+ # Corrects WHERE A PART LIVES in a response, per part, so a spec can fix
43
+ # one mapping without restating the others.
44
+ from?: &: string
45
+
46
+ }
47
+
6
48
  path: &: {
7
49
  active?: boolean
8
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/apidef",
3
- "version": "8.3.0",
3
+ "version": "8.5.0",
4
4
  "main": "dist/apidef.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/apidef.d.ts",
@@ -96,6 +96,26 @@ function migrateLegacyGuide(fs: any, folder: string, guideprefix: string): boole
96
96
  }
97
97
 
98
98
 
99
+ // The first unresolved merge-conflict marker in a source, or null.
100
+ //
101
+ // Anchored at line start and requiring exactly the conventional seven
102
+ // characters: a guide legitimately contains `>>>>>>> GENERATED` inside the
103
+ // jostraca provenance comments it writes about itself, and `====` shows up in
104
+ // prose. Only a real marker at column zero counts.
105
+ function findConflict(src: string): { line: number, text: string } | null {
106
+ const lines = String(src || '').split('\n')
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i]
110
+ if (/^(<{7}|>{7})(?!<|>)/.test(line) || /^={7}(?!=)\s*$/.test(line)) {
111
+ return { line: i + 1, text: line.slice(0, 80) }
112
+ }
113
+ }
114
+
115
+ return null
116
+ }
117
+
118
+
99
119
  async function buildGuide(ctx: ApiDefContext): Promise<any> {
100
120
  const log = ctx.log
101
121
  const errs: any[] = []
@@ -135,6 +155,60 @@ async function buildGuide(ctx: ApiDefContext): Promise<any> {
135
155
 
136
156
  handleErrors(ctx, errs)
137
157
 
158
+ // A MERGE CONFLICT IN A GUIDE IS SAID OUT LOUD, HERE.
159
+ //
160
+ // The guide is 3-way merged: apidef regenerates the base guide from the
161
+ // spec and merges it over what the project already had. Change the spec
162
+ // enough — swap a 5-path definition for the API's whole 722-path one — and
163
+ // an edit the project made can no longer be reconciled, so the merge
164
+ // writes ordinary `<<<<<<<` / `=======` / `>>>>>>>` markers into the file.
165
+ //
166
+ // Nothing then read the file until aontu did, and aontu reports what it
167
+ // sees: `unexpected character(s): <<<<<<<`, thousands of lines into a
168
+ // generated file, with no hint that this is a merge conflict or which edit
169
+ // caused it. That cost a long detour — the failure was read as apidef
170
+ // hanging, and the real cause (one conflicted rename) sat two lines away
171
+ // from a marker nobody had looked for.
172
+ //
173
+ // The guide is a file apidef itself writes, so apidef is the right place to
174
+ // recognise its own merge output before handing it on.
175
+ // BOTH FILES, and the base guide is the one that usually has it: `guide.aon`
176
+ // is two @-includes a user rarely edits, while `base-guide.aon` is what
177
+ // apidef regenerates and merges. Checking only the top-level file found
178
+ // nothing and left aontu to report the marker.
179
+ const basepath = Path.join(folder, 'guide', guideprefix + 'base-guide.aon')
180
+ for (const checkpath of [guidepath, basepath]) {
181
+ let checksrc = ''
182
+ try {
183
+ checksrc = checkpath === guidepath ? src : String(ctx.fs.readFileSync(checkpath, 'utf8'))
184
+ }
185
+ catch (_err: any) {
186
+ continue
187
+ }
188
+
189
+ const conflict = findConflict(checksrc)
190
+ if (null != conflict) {
191
+ errs.push(new Error(
192
+ `@voxgig/apidef: guide: unresolved merge conflict at ${
193
+ relativizePath(checkpath)}:${conflict.line}\n` +
194
+ ` ${conflict.text}\n` +
195
+ `A guide is merged, not overwritten, so an edit the regenerated base\n` +
196
+ `guide contradicts is left for a human to settle. Resolve the marked\n` +
197
+ `block` +
198
+ // DELETING ONLY HELPS FOR THE BASE GUIDE. Regeneration rewrites that
199
+ // file, while the top-level entry guide is the user's own and is read
200
+ // back unchanged — so advising its deletion would send a reader in a
201
+ // circle, failing this same check on the next build.
202
+ (checkpath === basepath ?
203
+ `, or delete ${guideprefix}base-guide.aon to regenerate it from the\n` +
204
+ `specification and re-apply the edit afterwards.` :
205
+ ` in ${relativizePath(checkpath)}.`)))
206
+ break
207
+ }
208
+ }
209
+
210
+ handleErrors(ctx, errs)
211
+
138
212
 
139
213
 
140
214
 
package/src/model.ts CHANGED
@@ -232,6 +232,29 @@ type ModelEntity = {
232
232
  id?: {
233
233
  name: string
234
234
  field: string
235
+ // COMPOSITE IDENTITY. Present only when the API addresses one record by
236
+ // MORE THAN ONE path parameter, so no single parameter is the id.
237
+ // github's repo is the case: GET /repos/{owner}/{repo} needs both, and
238
+ // neither alone names a repository.
239
+ //
240
+ // `parts` are those parameters in path order; `sep` joins them into the
241
+ // one `id` an SDK entity carries. Absent means the ordinary single-key
242
+ // entity, so downstream can branch on presence alone.
243
+ parts?: string[]
244
+ sep?: string
245
+ // WHERE EACH PART'S VALUE LIVES IN A RESPONSE, as a dotted path.
246
+ //
247
+ // The parts are PATH PARAMETER names and a response names its fields
248
+ // whatever it likes: github addresses a repo by `{owner}/{repo}` and
249
+ // returns the owner as an OBJECT, so the value is at `owner.login`, and
250
+ // the repository under `name`. Without this a consumer can address a
251
+ // record it was given the id of, but cannot put an id on a record the
252
+ // API returned.
253
+ //
254
+ // A part no rule resolves is left OUT, so an incomplete map says the id
255
+ // cannot be rebuilt for that entity — better than a confidently wrong id
256
+ // on a real record. guide.aon can state it instead.
257
+ from?: Record<string, string>
235
258
  }
236
259
  relations: ModelEntityRelations
237
260
  }