UNPKG

6.91 kBJavaScriptView Raw
1/*
2 * Copyright 2019 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12const ghslugger = require('github-slugger');
13const path = require('path');
14const { URL } = require('url');
15const { formatmeta } = require('./formatInfo');
16const symbols = require('./symbols');
17const { keyword } = require('./keywords');
18
19const myslug = Symbol('myslug');
20
21function loadExamples(file, num = 1) {
22 const examplefile = path.resolve(path.dirname(file), path.basename(file).replace(/\..*$/, `.example.${num}.json`));
23 try {
24 // eslint-disable-next-line import/no-dynamic-require, global-require
25 const example = require(examplefile);
26 return [example, ...loadExamples(file, num + 1)];
27 } catch {
28 return [];
29 }
30}
31
32const 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 // if the schema has it's own ID, use it
42 if (target[keyword`$id`]) {
43 return target[keyword`$id`];
44 }
45 if (parent) {
46 // if we can determine the parent ID (by walking up the tree, use it)
47 return parent[symbols.id];
48 }
49 return undefined;
50 };
51 meta[symbols.titles] = (target) => {
52 if (parent) {
53 // if we can determine the parent titles
54 // then we append our own
55 return [...parent[symbols.titles], target.title];
56 }
57 // otherwise, it's just our own
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 // console.log('trying to resolve', proppath, 'in', receiver[symbols.filename]);
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 // eslint-disable-next-line no-param-reassign
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 // eslint-disable-next-line no-param-reassign
89 receiver[myslug] = slugger.slug(`${parentslug}-${title || name}`);
90 } else {
91 // eslint-disable-next-line no-param-reassign
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 // console.log('resolving ref', uri, pointer, 'from', receiver[symbols.filename]);
116 const basedoc = uri || receiver[symbols.id];
117 let referenced = null;
118
119 // $ref is a URI-reference so basedoc might be an id URI or it might be a path
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 // if uri is a URI then only try to resolve it locally if the scheme is 'file:'
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 // console.log('Error parsing URL - ' + uri);
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 // inject the referenced schema into the current schema
150 Object.assign(retval, referenced);
151 } else {
152 console.error('cannot resolve', basedoc);
153 }
154 } else if (retval[symbols.filename]) {
155 // console.log('I am in a loop!');
156 return retval;
157 }
158
159 // console.log('making new proxy from', target, prop, 'receiver', receiver[symbols.id]);
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 // stow away the schema for lookup
170 // eslint-disable-next-line no-param-reassign
171 schemas.known[subschema[keyword`$id`]] = subschema;
172 }
173 return subschema;
174 }
175 return retval;
176 },
177 };
178};
179
180module.exports = {
181 ...symbols,
182};
183
184module.exports.loader = () => {
185 const schemas = {
186 loaded: [],
187 known: {},
188 files: {},
189 };
190
191 const slugger = ghslugger();
192
193 return (schema, filename) => {
194 // console.log('loading', filename);
195 const proxied = new Proxy(schema, handler({ filename, schemas, slugger }));
196 schemas.loaded.push(proxied);
197 if (proxied[keyword`$id`]) {
198 // stow away the schema for lookup
199 schemas.known[proxied[keyword`$id`]] = proxied;
200 }
201
202 schemas.files[filename] = proxied;
203
204 return proxied;
205 };
206};