ya-struct 0.0.11 → 0.0.13

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.
@@ -1,5 +1,6 @@
1
1
  import { define } from "./parser.js";
2
2
  import { types } from "./types/index.js";
3
+ import { compileAndCompare } from "./tests/compile-and-compare.vibe.js";
3
4
  import type { TAbi, TCompiler, TDataModel, TEndianness } from "./common.js";
4
- export { define, types };
5
+ export { define, types, compileAndCompare };
5
6
  export type { TAbi, TEndianness, TCompiler, TDataModel };
package/dist/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { define } from "./parser.js";
2
2
  import { types } from "./types/index.js";
3
+ import { compileAndCompare } from "./tests/compile-and-compare.vibe.js";
3
4
 
4
5
  /*import type {
5
6
  TAbi,
@@ -10,7 +11,8 @@ import { types } from "./types/index.js";
10
11
 
11
12
  export {
12
13
  define,
13
- types
14
+ types,
15
+ compileAndCompare
14
16
  };
15
17
 
16
18
  /*export type {
@@ -26,10 +26,15 @@ type TLayoutedField = {
26
26
  readonly type: "struct";
27
27
  readonly offsetInBits: number;
28
28
  readonly sizeInBits: number;
29
- readonly fields: readonly {
29
+ readonly fields: readonly ({
30
+ readonly pad?: false;
30
31
  readonly name: string;
31
32
  readonly definition: TLayoutedField;
32
- }[];
33
+ } | {
34
+ readonly pad: true;
35
+ readonly name: string | undefined;
36
+ readonly definition: TLayoutedField;
37
+ })[];
33
38
  readonly packed: boolean;
34
39
  readonly fixedAbi: Partial<TAbi>;
35
40
  } | {
@@ -29,7 +29,15 @@ import nodeUtil from "node:util";
29
29
  readonly type: "struct";
30
30
  readonly offsetInBits: number;
31
31
  readonly sizeInBits: number;
32
- readonly fields: readonly { readonly name: string; readonly definition: TLayoutedField }[];
32
+ readonly fields: readonly ({
33
+ readonly pad?: false;
34
+ readonly name: string;
35
+ readonly definition: TLayoutedField;
36
+ } | {
37
+ readonly pad: true;
38
+ readonly name: string | undefined;
39
+ readonly definition: TLayoutedField;
40
+ })[];
33
41
  readonly packed: boolean;
34
42
  readonly fixedAbi: Partial<TAbi>;
35
43
  } | {
@@ -51,6 +59,83 @@ const pointerSizeInBitsByDataModel = ({ dataModel }/*: { dataModel: TAbi["dataMo
51
59
  }
52
60
  };
53
61
 
62
+ // eslint-disable-next-line max-statements, complexity
63
+ const alignmentOfField = ({ definition, abi }/*: { definition: TFieldType, abi: TAbi }*/)/*: number*/ => {
64
+ if (definition.type === "c-type") {
65
+ const cTypeNormalizer = createCTypeNormalizer({ abi });
66
+ const normalizedField = cTypeNormalizer.normalize({ cField: definition });
67
+ return alignmentOfField({ definition: normalizedField, abi });
68
+ }
69
+
70
+ if (definition.type === "integer" || definition.type === "float") {
71
+ const sizeInBits = definition.sizeInBits;
72
+ if (abi.compiler === "gcc" && abi.dataModel === "ILP32" && sizeInBits === 64) {
73
+ return 32;
74
+ }
75
+ return sizeInBits;
76
+ }
77
+
78
+ if (definition.type === "pointer") {
79
+ return pointerSizeInBitsByDataModel({ dataModel: abi.dataModel });
80
+ }
81
+
82
+ if (definition.type === "array") {
83
+ return alignmentOfField({ definition: definition.elementType, abi });
84
+ }
85
+
86
+ if (definition.type === "struct") {
87
+ if (definition.packed) {
88
+ return 8;
89
+ }
90
+ if (definition.fields.length === 0) {
91
+ return 8;
92
+ }
93
+ return Math.max(...definition.fields.map((f) => {
94
+ return alignmentOfField({ definition: f.definition, abi });
95
+ }));
96
+ }
97
+
98
+ if (definition.type === "string") {
99
+ return definition.charSizeInBits;
100
+ }
101
+
102
+ throw Error(`unsupported field type for alignment calculation`);
103
+ };
104
+
105
+ // eslint-disable-next-line complexity
106
+ const translateLayoutOffset = ({ field, offset }/*: { field: TLayoutedField; offset: number }*/)/*: TLayoutedField*/ => {
107
+ if (offset === 0) {
108
+ return field;
109
+ }
110
+
111
+ switch (field.type) {
112
+ case "integer":
113
+ return { ...field, offsetInBits: field.offsetInBits + offset };
114
+ case "float":
115
+ return { ...field, offsetInBits: field.offsetInBits + offset };
116
+ case "pointer":
117
+ return { ...field, offsetInBits: field.offsetInBits + offset };
118
+ case "string":
119
+ return { ...field, offsetInBits: field.offsetInBits + offset };
120
+ case "array":
121
+ return { ...field, offsetInBits: field.offsetInBits + offset };
122
+ case "struct":
123
+ return {
124
+ ...field,
125
+ offsetInBits: field.offsetInBits + offset,
126
+ fields: field.fields.map((f) => {
127
+ const translated = translateLayoutOffset({ field: f.definition, offset });
128
+ if (f.pad) {
129
+ return { pad: true /*as const*/, name: f.name, definition: translated };
130
+ }
131
+ return { name: f.name, definition: translated };
132
+ })
133
+ };
134
+ default:
135
+ throw Error(`unsupported field type for layout offset translation`);
136
+ }
137
+ };
138
+
54
139
  const layoutStruct = ({
55
140
  definition,
56
141
  abi,
@@ -61,13 +146,13 @@ const layoutStruct = ({
61
146
  currentOffsetInBits: number
62
147
  }*/)/*: TLayoutedField*/ => {
63
148
 
64
- // TODO: implement alignment handling
65
- const structAlignmentInBits = 64;
66
- // const fieldAlignmentInBits = 64;
149
+ const structAlignmentInBits = definition.packed ? 8 : alignmentOfField({ definition, abi });
67
150
  const fieldAlignmentInBits = 1;
68
151
  const pointerSizeInBits = pointerSizeInBitsByDataModel({ dataModel: abi.dataModel });
69
152
 
70
- let currentOffsetInBits = align({ offset: initialOffsetInBits, alignment: structAlignmentInBits });
153
+ // Compute internal layout from offset 0 to ensure correct internal alignment
154
+ // independent of the struct's placement position
155
+ let currentOffsetInBits = 0;
71
156
  let layoutedFields/*: (TLayoutedField & { type: "struct" })["fields"]*/ = [];
72
157
 
73
158
  // eslint-disable-next-line max-statements,complexity
@@ -112,11 +197,8 @@ const layoutStruct = ({
112
197
  break;
113
198
  }
114
199
  case "struct": {
115
-
116
- if (currentOffsetInBits % 64 !== 0) {
117
- throw Error("nested struct alignment handling not implemented yet");
118
- }
119
-
200
+ const nestedAlignment = alignmentOfField({ definition: normalizedField, abi });
201
+ currentOffsetInBits = align({ offset: currentOffsetInBits, alignment: nestedAlignment });
120
202
  break;
121
203
  }
122
204
  case "string": {
@@ -137,20 +219,34 @@ const layoutStruct = ({
137
219
 
138
220
  layoutedFields = [
139
221
  ...layoutedFields,
140
- {
141
- name: field.name,
142
- definition: fieldLayout
143
- }
222
+ field.pad
223
+ ? { pad: true /*as const*/, name: field.name, definition: fieldLayout }
224
+ : { name: field.name, definition: fieldLayout }
144
225
  ];
145
226
 
146
227
  currentOffsetInBits = align({ offset: fieldLayout.offsetInBits + fieldLayout.sizeInBits, alignment: fieldAlignmentInBits });
147
228
  });
148
229
 
230
+ if (!definition.packed) {
231
+ currentOffsetInBits = align({ offset: currentOffsetInBits, alignment: structAlignmentInBits });
232
+ }
233
+
234
+ const sizeInBits = currentOffsetInBits;
235
+
236
+ // Translate all field offsets to the actual placement position
237
+ const translatedFields = layoutedFields.map((f) => {
238
+ const translated = translateLayoutOffset({ field: f.definition, offset: initialOffsetInBits });
239
+ if (f.pad) {
240
+ return { pad: true /*as const*/, name: f.name, definition: translated };
241
+ }
242
+ return { name: f.name, definition: translated };
243
+ });
244
+
149
245
  return {
150
246
  type: "struct",
151
247
  offsetInBits: initialOffsetInBits,
152
- sizeInBits: currentOffsetInBits - initialOffsetInBits,
153
- fields: layoutedFields,
248
+ sizeInBits,
249
+ fields: translatedFields,
154
250
  packed: definition.packed,
155
251
  fixedAbi: definition.fixedAbi
156
252
  };
@@ -16,20 +16,42 @@ type FieldValue<T extends TFieldType> = T extends {
16
16
  } ? FieldValue<E & TFieldType>[] : T extends {
17
17
  type: "struct";
18
18
  fields: infer F;
19
- } ? StructValue<F & readonly {
19
+ } ? StructValue<F & readonly ({
20
+ pad?: false | undefined;
20
21
  name: string;
21
22
  definition: TFieldType;
22
- }[]> : T extends {
23
+ } | {
24
+ pad: true;
25
+ name: string | undefined;
26
+ definition: TFieldType;
27
+ })[]> : T extends {
23
28
  type: "c-type";
24
29
  cType: "float" | "double" | "long double";
25
30
  } ? number : T extends {
26
31
  type: "c-type";
27
32
  } ? bigint : never;
28
- type StructValue<F extends readonly {
33
+ type NonPadFields<F extends readonly ({
34
+ pad?: false | undefined;
35
+ name: string;
36
+ definition: TFieldType;
37
+ } | {
38
+ pad: true;
39
+ name: string | undefined;
40
+ definition: TFieldType;
41
+ })[]> = Extract<F[number], {
42
+ pad?: false | undefined;
43
+ name: string;
44
+ }>;
45
+ type StructValue<F extends readonly ({
46
+ pad?: false | undefined;
29
47
  name: string;
30
48
  definition: TFieldType;
31
- }[]> = {
32
- [K in F[number] as K["name"]]: FieldValue<K["definition"]>;
49
+ } | {
50
+ pad: true;
51
+ name: string | undefined;
52
+ definition: TFieldType;
53
+ })[]> = {
54
+ [K in NonPadFields<F> as K["name"]]: FieldValue<K["definition"]>;
33
55
  };
34
56
  type Simplify<T> = {
35
57
  [K in keyof T]: T[K];
@@ -54,3 +76,4 @@ declare const define: <const T extends TFieldType>({ definition }: {
54
76
  }) => TParser<T>;
55
77
  };
56
78
  export { define };
79
+ export type { TParser, };
@@ -12,15 +12,34 @@ import { createStructParser } from "./types/struct.js";
12
12
  T extends { type: "array"; elementType: infer E; length: number }
13
13
  ? FieldValue<E & TFieldType>[] :
14
14
  T extends { type: "struct"; fields: infer F }
15
- ? StructValue<F & readonly { name: string; definition: TFieldType }[]> :
16
- T extends { type: "c-type"; cType: "float" | "double" | "long double" } ? number :
15
+ ? StructValue<
16
+ F & readonly (
17
+ | { pad?: false | undefined; name: string; definition: TFieldType }
18
+ | { pad: true; name: string | undefined; definition: TFieldType }
19
+ )[]
20
+ > :
21
+ T extends {
22
+ type: "c-type";
23
+ cType: "float" | "double" | "long double";
24
+ } ? number :
17
25
  T extends { type: "c-type" } ? bigint :
18
26
  never;*/
19
27
 
28
+ /*type NonPadFields<
29
+ F extends readonly (
30
+ | { pad?: false | undefined; name: string; definition: TFieldType }
31
+ | { pad: true; name: string | undefined; definition: TFieldType }
32
+ )[]
33
+ > =
34
+ Extract<F[number], { pad?: false | undefined; name: string }>;*/
35
+
20
36
  /*type StructValue<
21
- F extends readonly { name: string; definition: TFieldType }[]
37
+ F extends readonly (
38
+ | { pad?: false | undefined; name: string; definition: TFieldType }
39
+ | { pad: true; name: string | undefined; definition: TFieldType }
40
+ )[]
22
41
  > = {
23
- [K in F[number]as K["name"]]: FieldValue<K["definition"]>;
42
+ [K in NonPadFields<F> as K["name"]]: FieldValue<K["definition"]>;
24
43
  };*/
25
44
 
26
45
  /*type Simplify<T> = {
@@ -88,3 +107,7 @@ const define = /*<const T extends TFieldType>*/({ definition }/*: { definition:
88
107
  export {
89
108
  define
90
109
  };
110
+
111
+ /*export type {
112
+ TParser,
113
+ };*/
@@ -0,0 +1,30 @@
1
+ import type { TAbi } from "../common.js";
2
+ import type { TFieldType } from "../types/index.js";
3
+ type TLayoutError = {
4
+ type: "size-mismatch";
5
+ fieldPath: string;
6
+ expectedSizeInBits: number;
7
+ actualSizeInBits: number;
8
+ } | {
9
+ type: "offset-mismatch";
10
+ fieldPath: string;
11
+ expectedOffsetInBits: number;
12
+ actualOffsetInBits: number;
13
+ };
14
+ type TCompileAndCompareResult = {
15
+ layoutErrors: TLayoutError[];
16
+ };
17
+ declare const compileAndCompare: ({ structDefinition, abi, globalCode, cStructName, compileAndRun }: {
18
+ structDefinition: Extract<TFieldType, {
19
+ type: "struct";
20
+ }>;
21
+ abi: TAbi;
22
+ globalCode: string;
23
+ cStructName: string;
24
+ compileAndRun: ({ sourceCode }: {
25
+ sourceCode: string;
26
+ }) => Promise<{
27
+ output: string;
28
+ }>;
29
+ }) => Promise<TCompileAndCompareResult>;
30
+ export { compileAndCompare };
@@ -0,0 +1,225 @@
1
+ /*import type { TAbi } from "../common.ts";*/
2
+ /*import type { TLayoutedField } from "../layout.ts";*/
3
+ import { define } from "../parser.js";
4
+ /*import type { TFieldType } from "../types/index.ts";*/
5
+
6
+ /*type TLayoutError = {
7
+ type: "size-mismatch",
8
+ fieldPath: string;
9
+ expectedSizeInBits: number;
10
+ actualSizeInBits: number;
11
+ } | {
12
+ type: "offset-mismatch",
13
+ fieldPath: string;
14
+ expectedOffsetInBits: number;
15
+ actualOffsetInBits: number;
16
+ };*/
17
+
18
+ /*type TCompileAndCompareResult = {
19
+ layoutErrors: TLayoutError[];
20
+ };*/
21
+
22
+ /*type TFlatField = {
23
+ fieldPath: string;
24
+ offsetInBits: number;
25
+ sizeInBits: number;
26
+ isStruct: boolean;
27
+ };*/
28
+
29
+ /*type TStructLayoutedFields =
30
+ (TLayoutedField & { type: "struct" })["fields"];*/
31
+
32
+ const flattenLayout = ({
33
+ fields,
34
+ parentPath
35
+ }/*: {
36
+ fields: TStructLayoutedFields;
37
+ parentPath: string;
38
+ }*/)/*: TFlatField[]*/ => {
39
+ return fields.flatMap((field) => {
40
+ if (field.pad) {
41
+ return [];
42
+ }
43
+
44
+ const fieldPath = parentPath
45
+ ? `${parentPath}.${field.name}`
46
+ : field.name/*!*/;
47
+
48
+ const isStruct = field.definition.type === "struct";
49
+
50
+ const entry/*: TFlatField*/ = {
51
+ fieldPath,
52
+ offsetInBits: field.definition.offsetInBits,
53
+ sizeInBits: field.definition.sizeInBits,
54
+ isStruct,
55
+ };
56
+
57
+ if (isStruct) {
58
+ return [
59
+ entry,
60
+ ...flattenLayout({
61
+ fields: field.definition.fields,
62
+ parentPath: fieldPath,
63
+ }),
64
+ ];
65
+ }
66
+
67
+ return [entry];
68
+ });
69
+ };
70
+
71
+ const generateSourceCode = ({
72
+ globalCode,
73
+ cStructName,
74
+ flatFields,
75
+ }/*: {
76
+ globalCode: string;
77
+ cStructName: string;
78
+ flatFields: TFlatField[];
79
+ }*/) => {
80
+
81
+ const printStatements = flatFields.map((f, idx) => {
82
+ if (f.isStruct) {
83
+ const offsetExpr =
84
+ `offsetof(struct ${cStructName}, ${f.fieldPath}) * CHAR_BIT`;
85
+ const sizeExpr =
86
+ `sizeof(((struct ${cStructName}*)0)->${f.fieldPath}) * CHAR_BIT`;
87
+
88
+ return ` printf("%zu %zu\\n", `
89
+ + `(size_t)(${offsetExpr}), (size_t)(${sizeExpr}));`;
90
+ }
91
+
92
+ // Use memory scanning to detect actual bit offset and size.
93
+ // This works for both regular fields and bitfields.
94
+ return ` {
95
+ struct ${cStructName} __s${idx};
96
+ memset(&__s${idx}, 0, sizeof(__s${idx}));
97
+ memset(&__s${idx}.${f.fieldPath}, 0xFF, sizeof(__s${idx}.${f.fieldPath}));
98
+ unsigned char *__p${idx} = (unsigned char *)&__s${idx};
99
+ size_t __off${idx} = 0, __sz${idx} = 0;
100
+ int __found${idx} = 0;
101
+ for (size_t __i = 0; __i < sizeof(struct ${cStructName}) * CHAR_BIT; __i++) {
102
+ if (__p${idx}[__i / CHAR_BIT] & (1u << (__i % CHAR_BIT))) {
103
+ if (!__found${idx}) { __off${idx} = __i; __found${idx} = 1; }
104
+ __sz${idx}++;
105
+ }
106
+ }
107
+ printf("%zu %zu\\n", __off${idx}, __sz${idx});
108
+ }`;
109
+ }).join("\n");
110
+
111
+ return `#include <stdio.h>
112
+ #include <stddef.h>
113
+ #include <limits.h>
114
+ #include <string.h>
115
+ ${globalCode}
116
+
117
+ int main(void) {
118
+ ${printStatements}
119
+ return 0;
120
+ }
121
+ `;
122
+ };
123
+
124
+ const parseCOutput = ({
125
+ output,
126
+ }/*: {
127
+ output: string;
128
+ }*/)/*: { offset: number; size: number }[]*/ => {
129
+ return output.trim().split("\n").map((line) => {
130
+ const parts = line.trim().split(" ");
131
+ return {
132
+ offset: parseInt(parts[0], 10),
133
+ size: parseInt(parts[1], 10),
134
+ };
135
+ });
136
+ };
137
+
138
+ const compareLayouts = ({
139
+ flatFields,
140
+ cFields,
141
+ }/*: {
142
+ flatFields: TFlatField[];
143
+ cFields: { offset: number; size: number }[];
144
+ }*/)/*: TLayoutError[]*/ => {
145
+ return flatFields.flatMap((field, idx) => {
146
+ const cField = cFields[idx];
147
+ if (!cField) {
148
+ return [];
149
+ }
150
+
151
+ const sizeMismatch/*: TLayoutError[]*/ =
152
+ cField.size === field.sizeInBits
153
+ ? []
154
+ : [{
155
+ type: "size-mismatch" /*as const*/,
156
+ fieldPath: field.fieldPath,
157
+ expectedSizeInBits: cField.size,
158
+ actualSizeInBits: field.sizeInBits,
159
+ }];
160
+
161
+ const offsetMismatch/*: TLayoutError[]*/ =
162
+ cField.offset === field.offsetInBits
163
+ ? []
164
+ : [{
165
+ type: "offset-mismatch" /*as const*/,
166
+ fieldPath: field.fieldPath,
167
+ expectedOffsetInBits: cField.offset,
168
+ actualOffsetInBits: field.offsetInBits,
169
+ }];
170
+
171
+ return [...sizeMismatch, ...offsetMismatch];
172
+ });
173
+ };
174
+
175
+ const compileAndCompare = async ({
176
+ structDefinition,
177
+ abi,
178
+
179
+ globalCode,
180
+ cStructName,
181
+
182
+ compileAndRun
183
+ }/*: {
184
+ structDefinition: Extract<TFieldType, { type: "struct" }>;
185
+ abi: TAbi;
186
+
187
+ globalCode: string;
188
+ cStructName: string;
189
+
190
+ compileAndRun: ({ sourceCode }: { sourceCode: string }) => Promise<{
191
+ output: string;
192
+ }>;
193
+ }*/)/*: Promise<TCompileAndCompareResult>*/ => {
194
+
195
+ const def = define({ definition: structDefinition });
196
+ const parser = def.parser({ abi });
197
+ const layoutResult = parser.layout;
198
+
199
+ if (layoutResult.type !== "struct") {
200
+ throw Error("expected struct layout");
201
+ }
202
+
203
+ const flatFields = flattenLayout({
204
+ fields: layoutResult.fields,
205
+ parentPath: "",
206
+ });
207
+
208
+ const sourceCode = generateSourceCode({
209
+ globalCode,
210
+ cStructName,
211
+ flatFields,
212
+ });
213
+
214
+ const { output } = await compileAndRun({ sourceCode });
215
+
216
+ const cFields = parseCOutput({ output });
217
+
218
+ const layoutErrors = compareLayouts({ flatFields, cFields });
219
+
220
+ return { layoutErrors };
221
+ };
222
+
223
+ export {
224
+ compileAndCompare
225
+ };
@@ -18,10 +18,15 @@ type TFieldType = {
18
18
  readonly length: number;
19
19
  } | {
20
20
  readonly type: "struct";
21
- readonly fields: readonly {
21
+ readonly fields: readonly ({
22
+ readonly pad?: false | undefined;
22
23
  readonly name: string;
23
24
  readonly definition: TFieldType;
24
- }[];
25
+ } | {
26
+ readonly pad: true;
27
+ readonly name: string | undefined;
28
+ readonly definition: TFieldType;
29
+ })[];
25
30
  readonly packed: boolean;
26
31
  readonly fixedAbi: Partial<TAbi>;
27
32
  } | {
@@ -26,7 +26,15 @@
26
26
  readonly length: number;
27
27
  } | {
28
28
  readonly type: "struct";
29
- readonly fields: readonly { readonly name: string; readonly definition: TFieldType }[];
29
+ readonly fields: readonly ({
30
+ readonly pad?: false | undefined;
31
+ readonly name: string;
32
+ readonly definition: TFieldType
33
+ } | {
34
+ readonly pad: true;
35
+ readonly name: string | undefined;
36
+ readonly definition: TFieldType;
37
+ })[];
30
38
  readonly packed: boolean;
31
39
  readonly fixedAbi: Partial<TAbi>;
32
40
  } | {
@@ -78,6 +78,10 @@ const createStructParser = ({
78
78
  let result/*: Record<string, unknown>*/ = {};
79
79
 
80
80
  layoutedFields.forEach((field, idx) => {
81
+ if (field.pad) {
82
+ return;
83
+ }
84
+
81
85
  const fieldParser = fieldParsers[idx];
82
86
 
83
87
  // field definition is absolute, subtracting struct offset gives bit offset inside struct
@@ -99,7 +103,7 @@ const createStructParser = ({
99
103
  });
100
104
 
101
105
  // eslint-disable-next-line immutable/no-mutation -- performance
102
- result[field.name] = fieldParser.parse({ data: fieldData, offsetInBits: fieldOffsetInBits });
106
+ result[field.name/*!*/] = fieldParser.parse({ data: fieldData, offsetInBits: fieldOffsetInBits });
103
107
  });
104
108
 
105
109
  return result;
@@ -111,7 +115,11 @@ const createStructParser = ({
111
115
  }
112
116
 
113
117
  layoutedFields.forEach((field, idx) => {
114
- const fieldValue = value[field.name];
118
+ if (field.pad) {
119
+ return;
120
+ }
121
+
122
+ const fieldValue = value[field.name/*!*/];
115
123
  const fieldParser = fieldParsers[idx];
116
124
 
117
125
  const offsetInBitsInByte = field.definition.offsetInBits % 8;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.11",
2
+ "version": "0.0.13",
3
3
  "name": "ya-struct",
4
4
  "type": "module",
5
5
  "description": "Yet Another Node.js Structure API",