@voxgig/apidef 2.0.0 → 2.1.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.
Files changed (48) hide show
  1. package/dist/apidef.js +14 -20
  2. package/dist/apidef.js.map +1 -1
  3. package/dist/builder/flow/flowHeuristic01.js +37 -9
  4. package/dist/builder/flow/flowHeuristic01.js.map +1 -1
  5. package/dist/builder/flow.js +3 -3
  6. package/dist/builder/flow.js.map +1 -1
  7. package/dist/guide/heuristic01.js +197 -117
  8. package/dist/guide/heuristic01.js.map +1 -1
  9. package/dist/guide.js +37 -11
  10. package/dist/guide.js.map +1 -1
  11. package/dist/parse.d.ts +2 -1
  12. package/dist/parse.js +81 -3
  13. package/dist/parse.js.map +1 -1
  14. package/dist/transform/clean.d.ts +3 -0
  15. package/dist/transform/clean.js +16 -0
  16. package/dist/transform/clean.js.map +1 -0
  17. package/dist/transform/entity.js +21 -2
  18. package/dist/transform/entity.js.map +1 -1
  19. package/dist/transform/field.js +24 -1
  20. package/dist/transform/field.js.map +1 -1
  21. package/dist/transform/operation.js +131 -47
  22. package/dist/transform/operation.js.map +1 -1
  23. package/dist/transform.d.ts +1 -8
  24. package/dist/transform.js +121 -95
  25. package/dist/transform.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/types.d.ts +9 -0
  28. package/dist/types.js +3 -2
  29. package/dist/types.js.map +1 -1
  30. package/dist/utility.d.ts +7 -1
  31. package/dist/utility.js +85 -32
  32. package/dist/utility.js.map +1 -1
  33. package/model/apidef.jsonic +1 -0
  34. package/package.json +8 -9
  35. package/src/apidef.ts +23 -33
  36. package/src/builder/flow/flowHeuristic01.ts +44 -9
  37. package/src/builder/flow.ts +4 -3
  38. package/src/guide/heuristic01.ts +281 -124
  39. package/src/guide.ts +49 -14
  40. package/src/parse.ts +106 -4
  41. package/src/transform/clean.ts +28 -0
  42. package/src/transform/entity.ts +26 -3
  43. package/src/transform/field.ts +27 -1
  44. package/src/transform/operation.ts +203 -64
  45. package/src/transform.ts +29 -23
  46. package/src/types.ts +3 -2
  47. package/src/utility.ts +113 -1
  48. package/src/builder/flow/flowHeuristic01.ts~ +0 -45
@@ -1,11 +1,14 @@
1
1
 
2
2
 
3
- import { each, getx } from 'jostraca'
3
+ import { each, getx, snakify } from 'jostraca'
4
+
5
+ import { transform, setprop, getprop } from '@voxgig/struct'
4
6
 
5
7
  import type { TransformResult } from '../transform'
6
8
 
7
9
  import { fixName, OPKIND } from '../transform'
8
10
 
11
+ import { depluralize } from '../utility'
9
12
 
10
13
 
11
14
  const operationTransform = async function(
@@ -20,30 +23,52 @@ const operationTransform = async function(
20
23
 
21
24
  let msg = 'operations: '
22
25
 
23
- const paramBuilder = (paramMap: any, paramDef: any,
24
- entityModel: any, pathdef: any,
25
- op: any, path: any, entity: any, model: any) => {
26
-
27
- paramMap[paramDef.name] = {
26
+ const paramBuilder = (
27
+ paramMap: any,
28
+ paramDef: any,
29
+ opModel: any,
30
+ entityModel: any,
31
+ pathdef: any,
32
+ op: any,
33
+ path: any,
34
+ entity: any,
35
+ model: any
36
+ ) => {
37
+ const paramSpec: any = paramMap[paramDef.name] = {
28
38
  required: paramDef.required
29
39
  }
30
- fixName(paramMap[paramDef.name], paramDef.name)
40
+ fixName(paramSpec, paramDef.name)
31
41
 
32
42
  const type = paramDef.schema ? paramDef.schema.type : paramDef.type
33
- fixName(paramMap[paramDef.name], type, 'type')
43
+ fixName(paramSpec, type, 'type')
44
+
45
+ // Path params are always required.
46
+ opModel.validate.params[paramDef.name] = `\`$${paramSpec.TYPE}\``
34
47
  }
35
48
 
36
49
 
37
- const queryBuilder = (queryMap: any, queryDef: any,
38
- entityModel: any, pathdef: any,
39
- op: any, path: any, entity: any, model: any) => {
40
- queryMap[queryDef.name] = {
50
+ const queryBuilder = (
51
+ queryMap: any,
52
+ queryDef: any,
53
+ opModel: any,
54
+ entityModel: any,
55
+ pathdef: any,
56
+ op: any,
57
+ path: any,
58
+ entity: any,
59
+ model: any
60
+ ) => {
61
+ const querySpec: any = queryMap[queryDef.name] = {
41
62
  required: queryDef.required
42
63
  }
43
64
  fixName(queryMap[queryDef.name], queryDef.name)
44
65
 
45
66
  const type = queryDef.schema ? queryDef.schema.type : queryDef.type
46
67
  fixName(queryMap[queryDef.name], type, 'type')
68
+
69
+ if (queryDef.required) {
70
+ opModel.validate.params[queryDef.name] = `\`$${querySpec.TYPE}\``
71
+ }
47
72
  }
48
73
 
49
74
 
@@ -57,6 +82,7 @@ const operationTransform = async function(
57
82
  pathdef: any
58
83
  ) => {
59
84
  let out
85
+ let why = 'none'
60
86
 
61
87
  if (null != op.transform?.[direction]) {
62
88
  out = op.transform[direction]
@@ -67,16 +93,12 @@ const operationTransform = async function(
67
93
  const mdef = pathdef[method]
68
94
 
69
95
  // TODO: fix getx
70
- const content = 'res' === kind ?
96
+ // const content = 'res' === kind ?
97
+ const content = 'resform' === direction ?
71
98
  (getx(mdef, 'responses.200.content') ||
72
99
  getx(mdef, 'responses.201.content')) :
73
100
  getx(mdef, 'requestBody.content')
74
101
 
75
- // console.log(entityModel)
76
- // console.log(mdef)
77
- // console.log(getx(mdef, 'responses.200.content'))
78
- // console.log(kind, method, pathdef, content)
79
-
80
102
  if (content) {
81
103
 
82
104
  const schema = content['application/json']?.schema
@@ -86,18 +108,22 @@ const operationTransform = async function(
86
108
  const resolveDirectionTransform =
87
109
  'resform' === direction ? resolveResponseTransform : resolveRequestTransform
88
110
 
89
- const transform = resolveDirectionTransform(
90
- op,
91
- kind,
92
- method,
93
- mdef,
94
- content,
95
- schema,
96
- propkeys
97
- )
111
+ //const [transform, why]
112
+ ;[out, why]
113
+ = resolveDirectionTransform(
114
+ entityModel,
115
+ op,
116
+ kind,
117
+ direction,
118
+ method,
119
+ mdef,
120
+ content,
121
+ schema,
122
+ propkeys
123
+ )
98
124
 
99
125
  // out = JSON.stringify(transform)
100
- out = transform
126
+ // out = transform
101
127
  }
102
128
  else {
103
129
  out = 'res' === kind ? '`body`' : '`reqdata`'
@@ -105,23 +131,27 @@ const operationTransform = async function(
105
131
  }
106
132
 
107
133
 
108
- return out
134
+ return [out, why]
109
135
  }
110
136
 
111
137
 
112
138
  const resolveResponseTransform = (
139
+ entityModel: any,
113
140
  op: any,
114
141
  kind: 'res' | 'req',
142
+ direction: 'resform' | 'reqform',
115
143
  method: string,
116
144
  mdef: any,
117
145
  content: any,
118
146
  schema: any,
119
147
  propkeys: any
120
- ) => {
148
+ ): [any, string] => {
149
+ let why = 'default'
121
150
  let transform: any = '`body`'
151
+ const properties = schema?.properties
122
152
 
123
- if (null == content || null == schema || null == propkeys) {
124
- return transform
153
+ if (null == content || null == schema || null == propkeys || null == properties) {
154
+ return [transform, why]
125
155
  }
126
156
 
127
157
  const opname = op.key$
@@ -129,13 +159,18 @@ const operationTransform = async function(
129
159
  if ('list' === opname) {
130
160
  if ('array' !== schema.type) {
131
161
  if (1 === propkeys.length) {
162
+ why = 'list-single-prop:' + propkeys[0]
132
163
  transform = '`body.' + propkeys[0] + '`'
133
164
  }
134
165
  else {
135
166
  // Use sub property that is an array
136
167
  for (let pk of propkeys) {
137
- if ('array' === schema.properties[pk]?.type) {
168
+ if ('array' === properties[pk]?.type) {
169
+ why = 'list-single-array:' + pk
138
170
  transform = '`body.' + pk + '`'
171
+
172
+ // TODO: if each item has prop === name of entity, use that, get with $EACH
173
+
139
174
  break
140
175
  }
141
176
  }
@@ -144,13 +179,15 @@ const operationTransform = async function(
144
179
  }
145
180
  else {
146
181
  if ('object' === schema.type) {
147
- if (null == schema.properties.id) {
182
+ if (null == properties.id) {
148
183
  if (1 === propkeys.length) {
184
+ why = 'map-single-prop:' + propkeys[0]
149
185
  transform = '`body.' + propkeys[0] + '`'
150
186
  }
151
187
  else {
152
188
  for (let pk of propkeys) {
153
- if (schema.properties[pk].properties?.id) {
189
+ if (properties[pk]?.properties?.id) {
190
+ why = 'map-sub-prop:' + pk
154
191
  transform = '`body.' + pk + '`'
155
192
  break
156
193
  }
@@ -160,23 +197,27 @@ const operationTransform = async function(
160
197
  }
161
198
  }
162
199
 
163
- return transform
200
+ return [transform, why]
164
201
  }
165
202
 
166
203
 
167
204
  const resolveRequestTransform = (
205
+ entityModel: any,
168
206
  op: any,
169
207
  kind: 'res' | 'req',
208
+ direction: 'resform' | 'reqform',
170
209
  method: string,
171
210
  mdef: any,
172
211
  content: any,
173
212
  schema: any,
174
213
  propkeys: any
175
- ) => {
176
- let transform: any = '`data`'
214
+ ): [any, string] => {
215
+ let transform: any = '`reqdata`'
216
+ let why = 'default'
217
+ const properties = schema?.properties
177
218
 
178
- if (null == content || null == schema || null == propkeys) {
179
- return transform
219
+ if (null == content || null == schema || null == propkeys || null == properties) {
220
+ return [transform, why]
180
221
  }
181
222
 
182
223
  const opname = op.key$
@@ -184,13 +225,15 @@ const operationTransform = async function(
184
225
  if ('list' === opname) {
185
226
  if ('array' !== schema.type) {
186
227
  if (1 === propkeys.length) {
187
- transform = { [propkeys[0]]: '`data`' }
228
+ why = 'list-single-prop:' + propkeys[0]
229
+ transform = { [propkeys[0]]: '`reqdata`' }
188
230
  }
189
231
  else {
190
232
  // Use sub property that is an array
191
233
  for (let pk of propkeys) {
192
- if ('array' === schema.properties[pk]?.type) {
193
- transform = { [pk]: '`data`' }
234
+ if ('array' === properties[pk]?.type) {
235
+ why = 'list-single-array:' + pk
236
+ transform = { [pk]: '`reqdata`' }
194
237
  break
195
238
  }
196
239
  }
@@ -199,14 +242,16 @@ const operationTransform = async function(
199
242
  }
200
243
  else {
201
244
  if ('object' === schema.type) {
202
- if (null == schema.properties.id) {
245
+ if (null == properties.id) {
203
246
  if (1 === propkeys.length) {
204
- transform = { [propkeys[0]]: '`data`' }
247
+ why = 'map-single-prop:' + propkeys[0]
248
+ transform = { [propkeys[0]]: '`reqdata`' }
205
249
  }
206
250
  else {
207
251
  for (let pk of propkeys) {
208
- if (schema.properties[pk].properties?.id) {
209
- transform = { [pk]: '`data`' }
252
+ if (properties[pk]?.properties?.id) {
253
+ why = 'map-sub-prop:' + pk
254
+ transform = { [pk]: '`reqdata`' }
210
255
  break
211
256
  }
212
257
  }
@@ -215,7 +260,7 @@ const operationTransform = async function(
215
260
  }
216
261
  }
217
262
 
218
- return transform
263
+ return [transform, why]
219
264
  }
220
265
 
221
266
 
@@ -225,37 +270,93 @@ const operationTransform = async function(
225
270
  const method = op.method
226
271
  const kind = OPKIND[opname]
227
272
 
228
- const em = entityModel.op[opname] = {
273
+ const [resform, resform_COMMENT] =
274
+ resolveTransform(entityModel, op, kind, 'resform', pathdef)
275
+
276
+ const [reqform, reqform_COMMENT] =
277
+ resolveTransform(entityModel, op, kind, 'reqform', pathdef)
278
+
279
+ const opModel = {
229
280
  path: path.key$,
281
+ pathalt: ([] as any[]),
282
+
230
283
  method,
231
284
  kind,
232
285
  param: {},
233
286
  query: {},
234
- // transform: {
235
- resform: resolveTransform(entityModel, op, kind, 'resform', pathdef),
236
- reqform: resolveTransform(entityModel, op, kind, 'reqform', pathdef),
237
- // }
287
+
288
+ resform_COMMENT: 'derivation: ' + resform_COMMENT,
289
+ resform,
290
+
291
+ reqform_COMMENT: 'derivation: ' + reqform_COMMENT,
292
+ reqform,
293
+
294
+ validate: {
295
+ params: { '`$OPEN`': true }
296
+ }
238
297
  }
239
298
 
240
- fixName(em, op.key$)
299
+ fixName(opModel, op.key$)
300
+
301
+ let params: any[] = []
241
302
 
242
303
  // Params are in the path
243
304
  if (0 < path.params$.length) {
244
- let params = getx(pathdef[method], 'parameters?in=path') || []
245
- if (Array.isArray(params)) {
246
- params.reduce((a: any, p: any) =>
247
- (paramBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.param)
248
- }
305
+ let sharedparams = getx(pathdef, 'parameters?in=path') || []
306
+ params = sharedparams.concat(
307
+ getx(pathdef[method], 'parameters?in=path') || []
308
+ )
309
+
310
+ // if (Array.isArray(params)) {
311
+ params.reduce((a: any, p: any) =>
312
+ (paramBuilder(a, p, opModel, entityModel,
313
+ pathdef, op, path, entity, model), a), opModel.param)
314
+ //}
249
315
  }
250
316
 
251
317
  // Queries are after the ?
252
- let queries = getx(pathdef[op.val$], 'parameters?in!=path') || []
253
- if (Array.isArray(queries)) {
254
- queries.reduce((a: any, p: any) =>
255
- (queryBuilder(a, p, entityModel, pathdef, op, path, entity, model), a), em.query)
318
+ let sharedqueries = getx(pathdef, 'parameters?in!=path') || []
319
+ let queries = sharedqueries.concat(getx(pathdef[method], 'parameters?in!=path') || [])
320
+ queries.reduce((a: any, p: any) =>
321
+ (queryBuilder(a, p, opModel, entityModel,
322
+ pathdef, op, path, entity, model), a), opModel.query)
323
+
324
+ let pathalt: any[] = []
325
+ const pathselector = makePathSelector(path.key$) // , params)
326
+ let before = false
327
+
328
+ if (null != entityModel.op[opname]) {
329
+ pathalt = entityModel.op[opname].pathalt
330
+
331
+ // Ordering for pathalts: most to least paramrs, then alphanumberic
332
+ for (let i = 0; i < pathalt.length; i++) {
333
+ let current = pathalt[i]
334
+ before =
335
+ pathselector.pn$ > current.pn$ ||
336
+ (pathselector.pn$ === current.pn$ &&
337
+ pathselector.path <= current.path)
338
+
339
+ if (before) {
340
+ pathalt = [
341
+ ...pathalt.slice(0, i),
342
+ pathselector,
343
+ ...pathalt.slice(i),
344
+ ]
345
+ break
346
+ }
347
+ }
256
348
  }
257
349
 
258
- return em
350
+ if (!before) {
351
+ pathalt.push(pathselector)
352
+ }
353
+
354
+ opModel.path = pathalt[pathalt.length - 1].path
355
+ opModel.pathalt = pathalt
356
+
357
+ entityModel.op[opname] = opModel
358
+
359
+ return opModel
259
360
  },
260
361
 
261
362
 
@@ -281,6 +382,22 @@ const operationTransform = async function(
281
382
 
282
383
  }
283
384
 
385
+ /*
386
+ console.dir(
387
+ transform({ guide }, {
388
+ entity: {
389
+ '`$PACK`': ['guide.entity', {
390
+ '`$KEY`': 'name',
391
+ op: {
392
+ // load: ['`$IF`', ['`$SELECT`',{path:{'`$ANY`':{op:{load:'`$EXISTS`'}}}}], {
393
+ load: ['`$IF`', 'path.*.op.load', {
394
+ path: () => 'foo'
395
+ }]
396
+ }
397
+ }]
398
+ }
399
+ }), { depth: null })
400
+ */
284
401
 
285
402
  each(guide.entity, (guideEntity: any) => {
286
403
  let opcount = 0
@@ -296,6 +413,8 @@ const operationTransform = async function(
296
413
  opcount++
297
414
  }
298
415
  })
416
+
417
+
299
418
  })
300
419
 
301
420
  msg += guideEntity.name + '=' + opcount + ' '
@@ -305,6 +424,26 @@ const operationTransform = async function(
305
424
  }
306
425
 
307
426
 
427
+ function makePathSelector(path: string) { // , params: any[]) {
428
+ let out: any = { path }
429
+
430
+ let pn$ = 0
431
+ for (const m of path.matchAll(/\/[^\/]+\/{([^}]+)\}/g)) {
432
+ const paramName = depluralize(snakify(m[1]))
433
+ out[paramName] = true
434
+ pn$++
435
+ }
436
+ out.pn$ = pn$
437
+
438
+ // for (let p of params) {
439
+ // setprop(out, p.name, getprop(out, p.name, false))
440
+ // console.log('SP', p.name, out[p.name])
441
+ // }
442
+
443
+ return out
444
+ }
445
+
446
+
308
447
  export {
309
448
  operationTransform
310
449
  }
package/src/transform.ts CHANGED
@@ -79,6 +79,7 @@ const GuideShape = Gubu({
79
79
  type Guide = ReturnType<typeof GuideShape>
80
80
 
81
81
 
82
+ /*
82
83
  async function resolveTransforms(ctx: TransformCtx): Promise<TransformSpec> {
83
84
  const { log, model: { main: { api: { guide } } } } = ctx
84
85
 
@@ -89,28 +90,27 @@ async function resolveTransforms(ctx: TransformCtx): Promise<TransformSpec> {
89
90
  // TODO: parameterize
90
91
  const defkind = 'openapi'
91
92
  const transformNames = guide.control.transform[defkind].order
92
- .split(/\s*,\s*/)
93
+ .split(/\s*,\s* /)
93
94
  .map((t: string) => t.trim())
94
- .filter((t: string) => '' != t)
95
+ .filter((t: string) => '' != t)
95
96
 
96
- log.info({
97
- point: 'transform', note: 'order: ' + transformNames.join(';'),
98
- order: transformNames
99
- })
97
+ log.info({
98
+ point: 'transform', note: 'order: ' + transformNames.join(';'),
99
+ order: transformNames
100
+ })
100
101
 
101
- try {
102
- for (const tn of transformNames) {
103
- log.debug({ what: 'transform', transform: tn, note: tn })
104
- const transform = await resolveTransform(tn, ctx)
105
- tspec.transform.push(transform)
106
- }
107
- }
108
- catch (err: any) {
109
- console.log(err)
110
- throw err
102
+ try {
103
+ for (const tn of transformNames) {
104
+ log.debug({ what: 'transform', transform: tn, note: tn })
105
+ const transform = await resolveTransform(tn, ctx)
106
+ tspec.transform.push(transform)
111
107
  }
108
+ }
109
+ catch (err: any) {
110
+ throw err
111
+ }
112
112
 
113
- return tspec
113
+ return tspec
114
114
  }
115
115
 
116
116
 
@@ -181,7 +181,7 @@ async function processTransforms(
181
181
  }
182
182
  catch (err: any) {
183
183
  // TODO: fix: this error does not get printed
184
- console.log(err)
184
+ console.error(err)
185
185
 
186
186
  pres.ok = false
187
187
  pres.msg += transform.name + ': ' + err.message + '\n'
@@ -196,13 +196,19 @@ async function processTransforms(
196
196
 
197
197
  return pres
198
198
  }
199
+ */
199
200
 
200
201
 
201
202
 
202
203
  function fixName(base: any, name: string, prop = 'name') {
203
- base[prop.toLowerCase()] = name.toLowerCase()
204
- base[camelify(prop)] = camelify(name)
205
- base[prop.toUpperCase()] = name.toUpperCase()
204
+ if (null != base && 'object' === typeof base && 'string' === typeof name) {
205
+ base[prop.toLowerCase()] = name.toLowerCase()
206
+ base[camelify(prop)] = camelify(name)
207
+ base[prop.toUpperCase()] = name.toUpperCase()
208
+ }
209
+ else {
210
+ // record to a "wierds" log
211
+ }
206
212
  }
207
213
 
208
214
 
@@ -222,6 +228,6 @@ export {
222
228
  fixName,
223
229
  OPKIND,
224
230
  GuideShape,
225
- resolveTransforms,
226
- processTransforms,
231
+ // resolveTransforms,
232
+ // processTransforms,
227
233
  }
package/src/types.ts CHANGED
@@ -29,11 +29,12 @@ const ModelShape = Gubu({
29
29
  sdk: {},
30
30
  def: {},
31
31
  api: {
32
- guide: {}
32
+ guide: {},
33
+ entity: {},
33
34
  },
34
35
  }
35
36
  })
36
- const OpenModelShape = Gubu(Open(ModelShape))
37
+ const OpenModelShape = Gubu(Open(ModelShape), { name: 'Model' })
37
38
 
38
39
  type Model = ReturnType<typeof ModelShape>
39
40
 
package/src/utility.ts CHANGED
@@ -8,6 +8,26 @@ import type {
8
8
  } from './types'
9
9
 
10
10
 
11
+ function getdlog(
12
+ tagin?: string,
13
+ filepath?: string)
14
+ : ((...args: any[]) => void) &
15
+ { tag: string, file: string, log: (fp?: string) => any[] } {
16
+ const tag = tagin || '-'
17
+ const file = Path.basename(filepath || '-')
18
+ const g = global as any
19
+ g.__dlog__ = (g.__dlog__ || [])
20
+ const dlog = (...args: any[]) =>
21
+ g.__dlog__.push([tag, file, Date.now(), ...args])
22
+ dlog.tag = tag
23
+ dlog.file = file
24
+ dlog.log = (filepath?: string, f?: string | null) =>
25
+ (f = null == filepath ? null : Path.basename(filepath),
26
+ g.__dlog__.filter((n: any[]) => n[0] === tag && (null == f || n[2] === f)))
27
+ return dlog
28
+ }
29
+
30
+
11
31
  function loadFile(path: string, what: string, fs: FsUtil, log: Log) {
12
32
  try {
13
33
  const source = fs.readFileSync(path, 'utf8')
@@ -24,9 +44,97 @@ function formatJsonSrc(jsonsrc: string) {
24
44
  return jsonsrc
25
45
  .replace(/"([a-zA-Z_][a-zA-Z_0-9]*)": /g, '$1: ')
26
46
  .replace(/},/g, '}\n')
47
+ // .replace(/([a-zA-Z_][a-zA-Z_0-9]*)_COMMENT:/g, '# $1')
48
+ .replace(/\n(\s*)([a-zA-Z_][a-zA-Z_0-9]*)_COMMENT:\s*"(.*)",/g, '\n\n$1# $2 $3')
49
+ }
50
+
51
+
52
+ function depluralize(word: string): string {
53
+ if (!word || word.length === 0) {
54
+ return word
55
+ }
56
+
57
+ // Common irregular plurals
58
+ const irregulars: Record<string, string> = {
59
+ 'analyses': 'analysis',
60
+ 'appendices': 'appendix',
61
+ 'axes': 'axis',
62
+ 'children': 'child',
63
+ 'courses': 'course',
64
+ 'crises': 'crisis',
65
+ 'criteria': 'criterion',
66
+ 'data': 'datum',
67
+ 'diagnoses': 'diagnosis',
68
+ 'feet': 'foot',
69
+ 'furnace': 'furnaces',
70
+ 'geese': 'goose',
71
+ 'horses': 'horse',
72
+ 'house': 'houses',
73
+ 'indices': 'index',
74
+ 'license': 'licenses',
75
+ 'matrices': 'matrix',
76
+ 'men': 'man',
77
+ 'mice': 'mouse',
78
+ 'notice': 'notices',
79
+ 'oases': 'oasis',
80
+ 'people': 'person',
81
+ 'phenomena': 'phenomenon',
82
+ 'practice': 'practices',
83
+ 'promise': 'promises',
84
+ 'teeth': 'tooth',
85
+ 'theses': 'thesis',
86
+ 'vertices': 'vertex',
87
+ 'women': 'woman',
88
+ }
89
+
90
+ if (irregulars[word]) {
91
+ return irregulars[word]
92
+ }
93
+
94
+ // Rules for regular plurals (applied in order)
95
+
96
+ if (word.endsWith('ies') && word.length > 3) {
97
+ return word.slice(0, -3) + 'y'
98
+ }
99
+
100
+
101
+ // -ies -> -y (cities -> city)
102
+ if (word.endsWith('ies') && word.length > 3) {
103
+ return word.slice(0, -3) + 'y'
104
+ }
105
+
106
+ // -ves -> -f or -fe (wolves -> wolf, knives -> knife)
107
+ if (word.endsWith('ves')) {
108
+ const stem = word.slice(0, -3)
109
+ // Check if it should be -fe (like knife, wife, life)
110
+ if (['kni', 'wi', 'li'].includes(stem)) {
111
+ return stem + 'fe'
112
+ }
113
+ return stem + 'f'
114
+ }
115
+
116
+ // -oes -> -o (potatoes -> potato)
117
+ if (word.endsWith('oes')) {
118
+ return word.slice(0, -2)
119
+ }
120
+
121
+ // -ses, -xes, -zes, -shes, -ches -> remove -es (boxes -> box)
122
+ if (word.endsWith('ses') || word.endsWith('xes') || word.endsWith('zes') ||
123
+ word.endsWith('shes') || word.endsWith('ches')) {
124
+ return word.slice(0, -2)
125
+ }
126
+
127
+ // -s -> remove -s (cats -> cat)
128
+ if (word.endsWith('s') && !word.endsWith('ss')) {
129
+ return word.slice(0, -1)
130
+ }
131
+
132
+ // If none of the rules apply, return as is
133
+ return word
27
134
  }
28
135
 
29
136
 
137
+ /*
30
138
  function writeChanged(
31
139
  point: string, path: string, content: string,
32
140
  fs: FsUtil, log: Log,
@@ -74,10 +182,14 @@ function writeChanged(
74
182
  throw err
75
183
  }
76
184
  }
185
+ */
77
186
 
78
187
 
79
188
  export {
189
+ getdlog,
80
190
  loadFile,
81
- formatJsonSrc
191
+ formatJsonSrc,
192
+ depluralize,
193
+
82
194
  // writeChanged,
83
195
  }