1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const ghslugger = require('github-slugger');
|
13 | const path = require('path');
|
14 | const { URL } = require('url');
|
15 | const { formatmeta } = require('./formatInfo');
|
16 | const symbols = require('./symbols');
|
17 | const { keyword } = require('./keywords');
|
18 |
|
19 | const myslug = Symbol('myslug');
|
20 |
|
21 | function loadExamples(file, num = 1) {
|
22 | const examplefile = path.resolve(path.dirname(file), path.basename(file).replace(/\..*$/, `.example.${num}.json`));
|
23 | try {
|
24 |
|
25 | const example = require(examplefile);
|
26 | return [example, ...loadExamples(file, num + 1)];
|
27 | } catch {
|
28 | return [];
|
29 | }
|
30 | }
|
31 |
|
32 | const handler = ({
|
33 | root = '', filename = '.', schemas, parent, slugger,
|
34 | }) => {
|
35 | const meta = {};
|
36 |
|
37 | meta[symbols.parent] = () => parent;
|
38 | meta[symbols.pointer] = () => root;
|
39 | meta[symbols.filename] = () => filename;
|
40 | meta[symbols.id] = (target) => {
|
41 |
|
42 | if (target[keyword`$id`]) {
|
43 | return target[keyword`$id`];
|
44 | }
|
45 | if (parent) {
|
46 |
|
47 | return parent[symbols.id];
|
48 | }
|
49 | return undefined;
|
50 | };
|
51 | meta[symbols.titles] = (target) => {
|
52 | if (parent) {
|
53 |
|
54 |
|
55 | return [...parent[symbols.titles], target.title];
|
56 | }
|
57 |
|
58 | if (typeof target.title === 'string') {
|
59 | return [target[keyword`title`]];
|
60 | }
|
61 | return [];
|
62 | };
|
63 |
|
64 | meta[symbols.resolve] = (target, prop, receiver) => (proppath) => {
|
65 |
|
66 | if (proppath === undefined) {
|
67 | return receiver;
|
68 | }
|
69 | const [head, ...tail] = typeof proppath === 'string' ? proppath.split('/') : proppath;
|
70 | if ((head === '' || head === undefined) && tail.length === 0) {
|
71 | return receiver;
|
72 | } else if (head === '' || head === undefined) {
|
73 | return receiver[symbols.resolve](tail);
|
74 | }
|
75 | return receiver[head] ? receiver[head][symbols.resolve](tail) : undefined;
|
76 | };
|
77 |
|
78 | meta[symbols.slug] = (target, prop, receiver) => {
|
79 | if (!receiver[myslug] && !parent && receiver[symbols.filename]) {
|
80 |
|
81 | receiver[myslug] = slugger.slug(path.basename(receiver[symbols.filename]).replace(/\..*$/, ''));
|
82 | }
|
83 | if (!receiver[myslug]) {
|
84 | const parentslug = parent[symbols.slug];
|
85 | const { title } = receiver;
|
86 | const name = receiver[symbols.pointer].split('/').pop();
|
87 | if (typeof title === 'string') {
|
88 |
|
89 | receiver[myslug] = slugger.slug(`${parentslug}-${title || name}`);
|
90 | } else {
|
91 |
|
92 | receiver[myslug] = slugger.slug(`${parentslug}-${name}`);
|
93 | }
|
94 | }
|
95 | return receiver[myslug];
|
96 | };
|
97 |
|
98 | meta[symbols.meta] = (target, prop, receiver) => formatmeta(receiver);
|
99 |
|
100 | return {
|
101 | ownKeys: (target) => Reflect.ownKeys(target),
|
102 |
|
103 | get: (target, prop, receiver) => {
|
104 | if (typeof meta[prop] === 'function') {
|
105 | return meta[prop](target, prop, receiver);
|
106 | }
|
107 |
|
108 | const retval = Reflect.get(target, prop, receiver);
|
109 | if (retval === undefined && prop === keyword`examples` && !receiver[symbols.parent]) {
|
110 | return loadExamples(receiver[symbols.filename], 1);
|
111 | }
|
112 | if (typeof retval === 'object' && retval !== null) {
|
113 | if (retval[keyword`$ref`]) {
|
114 | const [uri, pointer] = retval.$ref.split('#');
|
115 |
|
116 | const basedoc = uri || receiver[symbols.id];
|
117 | let referenced = null;
|
118 |
|
119 |
|
120 |
|
121 | if (schemas.known[basedoc]) {
|
122 | referenced = schemas.known[basedoc][symbols.resolve](pointer);
|
123 | } else if (path.parse(basedoc)) {
|
124 | const basepath = path.dirname(meta[symbols.filename]());
|
125 | let reldoc = uri;
|
126 |
|
127 |
|
128 | try {
|
129 | const urlinfo = new URL(uri);
|
130 | if (urlinfo.protocol === 'file:') {
|
131 | reldoc = uri.replace(/^file:\/\//, '');
|
132 | } else {
|
133 | reldoc = null;
|
134 | }
|
135 | } catch (err) {
|
136 |
|
137 | }
|
138 |
|
139 | if (reldoc) {
|
140 | const refpath = path.resolve(basepath, reldoc);
|
141 |
|
142 | if (schemas.files[refpath]) {
|
143 | referenced = schemas.files[refpath][symbols.resolve](pointer);
|
144 | }
|
145 | }
|
146 | }
|
147 |
|
148 | if (referenced !== null) {
|
149 |
|
150 | Object.assign(retval, referenced);
|
151 | } else {
|
152 | console.error('cannot resolve', basedoc);
|
153 | }
|
154 | } else if (retval[symbols.filename]) {
|
155 |
|
156 | return retval;
|
157 | }
|
158 |
|
159 |
|
160 | const subschema = new Proxy(retval, handler({
|
161 | root: `${root}/${prop}`,
|
162 | parent: receiver,
|
163 | filename,
|
164 | schemas,
|
165 | slugger,
|
166 | }));
|
167 |
|
168 | if (subschema[keyword`$id`]) {
|
169 |
|
170 |
|
171 | schemas.known[subschema[keyword`$id`]] = subschema;
|
172 | }
|
173 | return subschema;
|
174 | }
|
175 | return retval;
|
176 | },
|
177 | };
|
178 | };
|
179 |
|
180 | module.exports = {
|
181 | ...symbols,
|
182 | };
|
183 |
|
184 | module.exports.loader = () => {
|
185 | const schemas = {
|
186 | loaded: [],
|
187 | known: {},
|
188 | files: {},
|
189 | };
|
190 |
|
191 | const slugger = ghslugger();
|
192 |
|
193 | return (schema, filename) => {
|
194 |
|
195 | const proxied = new Proxy(schema, handler({ filename, schemas, slugger }));
|
196 | schemas.loaded.push(proxied);
|
197 | if (proxied[keyword`$id`]) {
|
198 |
|
199 | schemas.known[proxied[keyword`$id`]] = proxied;
|
200 | }
|
201 |
|
202 | schemas.files[filename] = proxied;
|
203 |
|
204 | return proxied;
|
205 | };
|
206 | };
|