@idfkit/core 0.1.0 → 0.2.0-rc.2
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/README.md +16 -7
- package/dist/collection.d.ts +0 -13
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +1 -1
- package/dist/conformance.d.ts +20 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +20 -0
- package/dist/conformance.js.map +1 -0
- package/dist/docs-url/index.d.ts +81 -0
- package/dist/docs-url/index.d.ts.map +1 -0
- package/dist/docs-url/index.js +201 -0
- package/dist/docs-url/index.js.map +1 -0
- package/dist/docs-url/locations.d.ts +2 -0
- package/dist/docs-url/locations.d.ts.map +1 -0
- package/dist/docs-url/locations.js +272 -0
- package/dist/docs-url/locations.js.map +1 -0
- package/dist/document.d.ts +40 -14
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +0 -0
- package/dist/document.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/introspect/describe.d.ts +117 -0
- package/dist/introspect/describe.d.ts.map +1 -0
- package/dist/introspect/describe.js +206 -0
- package/dist/introspect/describe.js.map +1 -0
- package/dist/node.d.ts +5 -5
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +25 -5
- package/dist/node.js.map +1 -1
- package/dist/object.d.ts +1 -1
- package/dist/parse/epjson.d.ts +1 -1
- package/dist/parse/epjson.d.ts.map +1 -1
- package/dist/parse/epjson.js +3 -3
- package/dist/parse/epjson.js.map +1 -1
- package/dist/parse/idf.d.ts +24 -6
- package/dist/parse/idf.d.ts.map +1 -1
- package/dist/parse/idf.js +181 -8
- package/dist/parse/idf.js.map +1 -1
- package/dist/parse/lexer.d.ts +36 -0
- package/dist/parse/lexer.d.ts.map +1 -1
- package/dist/parse/lexer.js +49 -2
- package/dist/parse/lexer.js.map +1 -1
- package/dist/typemap.d.ts +7 -5
- package/dist/typemap.d.ts.map +1 -1
- package/dist/typemap.js +7 -5
- package/dist/typemap.js.map +1 -1
- package/dist/validate/index.d.ts +11 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +10 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/types.d.ts +63 -0
- package/dist/validate/types.d.ts.map +1 -0
- package/dist/validate/types.js +25 -0
- package/dist/validate/types.js.map +1 -0
- package/dist/validate/validate.d.ts +82 -0
- package/dist/validate/validate.d.ts.map +1 -0
- package/dist/validate/validate.js +574 -0
- package/dist/validate/validate.js.map +1 -0
- package/dist/write/epjson.d.ts +3 -3
- package/dist/write/idf.d.ts +34 -2
- package/dist/write/idf.d.ts.map +1 -1
- package/dist/write/idf.js +24 -5
- package/dist/write/idf.js.map +1 -1
- package/package.json +11 -8
- package/dist/types/v26-1.d.ts +0 -65896
- package/dist/types/v26-1.d.ts.map +0 -1
- package/dist/types/v26-1.js +0 -5
- package/dist/types/v26-1.js.map +0 -1
- package/dist/types/v9-4.d.ts +0 -61414
- package/dist/types/v9-4.d.ts.map +0 -1
- package/dist/types/v9-4.js +0 -5
- package/dist/types/v9-4.js.map +0 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary validation reports in.
|
|
3
|
+
*
|
|
4
|
+
* A finding is a record, not a throw. The Python original names the type
|
|
5
|
+
* `ValidationError` and this port keeps that spelling, but nothing here is ever
|
|
6
|
+
* raised: a finding carries a severity and a location, and callers collect them
|
|
7
|
+
* into a `ValidationResult` and decide what to do. Naming it after an exception
|
|
8
|
+
* and then returning it is the one thing the naming register asks both
|
|
9
|
+
* libraries to keep in step, so the name stays.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Severity of a validation finding.
|
|
13
|
+
*
|
|
14
|
+
* Both a value and a type. The value gives the Python original's
|
|
15
|
+
* `Severity.ERROR` spelling; the type is the string union that a `'error'`
|
|
16
|
+
* literal satisfies, which is how the same three strings reach the wire in both
|
|
17
|
+
* languages. The strings themselves are load-bearing: the conformance corpus
|
|
18
|
+
* compares them across the two implementations.
|
|
19
|
+
*/
|
|
20
|
+
export const Severity = Object.freeze({
|
|
21
|
+
ERROR: 'error',
|
|
22
|
+
WARNING: 'warning',
|
|
23
|
+
INFO: 'info',
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/validate/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IACpC,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,MAAM;CACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand validation against the schema.
|
|
3
|
+
*
|
|
4
|
+
* A port of the Python library's `idfkit.validation`, kept deliberately close
|
|
5
|
+
* to it: the same checks in the same order producing the same `code` strings,
|
|
6
|
+
* so the conformance corpus can hold the two implementations against one
|
|
7
|
+
* another. Where the slim schema cannot express what the Python side reads out
|
|
8
|
+
* of the full epJSON schema, the difference is called out at the check rather
|
|
9
|
+
* than papered over.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is eager. Parsing never validates; you validate when you want
|
|
12
|
+
* to, over the object types you care about.
|
|
13
|
+
*/
|
|
14
|
+
import type { Schema } from '@idfkit/schemas';
|
|
15
|
+
import type { IdfDocument } from '../document.js';
|
|
16
|
+
import type { IdfObject } from '../object.js';
|
|
17
|
+
import type { AnyTypeMap, UntypedMap } from '../typemap.js';
|
|
18
|
+
import { type ValidationError, type ValidationResult } from './types.js';
|
|
19
|
+
/** Which checks a document run performs. Every one defaults to on. */
|
|
20
|
+
interface ValidateDocumentOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Schema to validate against. Defaults to the document's own, which is what
|
|
23
|
+
* you want unless you are asking "would this model load under 25.2?".
|
|
24
|
+
*/
|
|
25
|
+
schema?: Schema;
|
|
26
|
+
/** Report references whose target no object provides. */
|
|
27
|
+
checkReferences?: boolean;
|
|
28
|
+
/** Report required fields that are absent or blank. */
|
|
29
|
+
checkRequired?: boolean;
|
|
30
|
+
/** Report values whose type the schema does not allow. */
|
|
31
|
+
checkTypes?: boolean;
|
|
32
|
+
/** Report numeric values outside their declared bounds. */
|
|
33
|
+
checkRanges?: boolean;
|
|
34
|
+
/** Report singleton types present more than once. */
|
|
35
|
+
checkSingletons?: boolean;
|
|
36
|
+
/** Restrict the run to these object types. Omit for all of them. */
|
|
37
|
+
objectTypes?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** Which checks a single-object run performs. Every one defaults to on. */
|
|
40
|
+
interface ValidateObjectOptions {
|
|
41
|
+
/** Report required fields that are absent or blank. */
|
|
42
|
+
checkRequired?: boolean;
|
|
43
|
+
/** Report values whose type the schema does not allow. */
|
|
44
|
+
checkTypes?: boolean;
|
|
45
|
+
/** Report numeric values outside their declared bounds. */
|
|
46
|
+
checkRanges?: boolean;
|
|
47
|
+
/** Report fields the schema does not define. */
|
|
48
|
+
checkUnknown?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a whole document.
|
|
52
|
+
*
|
|
53
|
+
* Findings are split by severity; `errors` being empty is what `isValid` means.
|
|
54
|
+
* Nothing throws: a model that cannot load is described, not raised.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const result = validateDocument(doc);
|
|
59
|
+
* if (!result.isValid) {
|
|
60
|
+
* for (const error of result.errors) console.error(error.code, error.message);
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateDocument<M extends AnyTypeMap = UntypedMap>(doc: IdfDocument<M>, options?: ValidateDocumentOptions): ValidationResult;
|
|
65
|
+
/**
|
|
66
|
+
* Validate one object, attached or detached.
|
|
67
|
+
*
|
|
68
|
+
* Useful right after building an object, before it goes anywhere near a
|
|
69
|
+
* document. The schema is passed explicitly rather than taken from the object
|
|
70
|
+
* so that "does this object hold up under another EnergyPlus version?" is
|
|
71
|
+
* askable; a type the schema does not define is reported as `W002` rather than
|
|
72
|
+
* throwing.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const material = doc.add('Material', 'Gypsum', { roughness: 'Smooth' });
|
|
77
|
+
* const findings = validateObject(material, doc.schema);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function validateObject(obj: IdfObject, schema: Schema, options?: ValidateObjectOptions): ValidationError[];
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/validate/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnF,sEAAsE;AACtE,UAAU,uBAAuB;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oEAAoE;IACpE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,2EAA2E;AAC3E,UAAU,qBAAqB;IAC7B,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EAChE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,EACnB,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAwFlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,eAAe,EAAE,CAOnB"}
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand validation against the schema.
|
|
3
|
+
*
|
|
4
|
+
* A port of the Python library's `idfkit.validation`, kept deliberately close
|
|
5
|
+
* to it: the same checks in the same order producing the same `code` strings,
|
|
6
|
+
* so the conformance corpus can hold the two implementations against one
|
|
7
|
+
* another. Where the slim schema cannot express what the Python side reads out
|
|
8
|
+
* of the full epJSON schema, the difference is called out at the check rather
|
|
9
|
+
* than papered over.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is eager. Parsing never validates; you validate when you want
|
|
12
|
+
* to, over the object types you care about.
|
|
13
|
+
*/
|
|
14
|
+
import { DATA } from '../internal.js';
|
|
15
|
+
import { Severity } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Validate a whole document.
|
|
18
|
+
*
|
|
19
|
+
* Findings are split by severity; `errors` being empty is what `isValid` means.
|
|
20
|
+
* Nothing throws: a model that cannot load is described, not raised.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const result = validateDocument(doc);
|
|
25
|
+
* if (!result.isValid) {
|
|
26
|
+
* for (const error of result.errors) console.error(error.code, error.message);
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function validateDocument(doc, options = {}) {
|
|
31
|
+
const schema = options.schema ?? doc.schema;
|
|
32
|
+
const checkReferences = options.checkReferences ?? true;
|
|
33
|
+
const checkSingletons = options.checkSingletons ?? true;
|
|
34
|
+
const checks = {
|
|
35
|
+
required: options.checkRequired ?? true,
|
|
36
|
+
types: options.checkTypes ?? true,
|
|
37
|
+
ranges: options.checkRanges ?? true,
|
|
38
|
+
// The Python original does not expose `check_unknown` on the document
|
|
39
|
+
// entry point, so a document run always makes it.
|
|
40
|
+
unknown: true,
|
|
41
|
+
};
|
|
42
|
+
const errors = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const info = [];
|
|
45
|
+
// Types actually present, canonicalized, in document order. Asking the
|
|
46
|
+
// document for a collection it does not have would create an empty one as a
|
|
47
|
+
// side effect, so membership is settled before any lookup.
|
|
48
|
+
const present = new Set(doc.types());
|
|
49
|
+
const requested = options.objectTypes;
|
|
50
|
+
const typesToCheck = requested === undefined
|
|
51
|
+
? [...present]
|
|
52
|
+
: requested.map((name) => schema.resolve(name) ?? name).filter((name) => present.has(name));
|
|
53
|
+
if (checkSingletons) {
|
|
54
|
+
for (const objType of typesToCheck) {
|
|
55
|
+
if (schema.get(objType)?.s !== 1)
|
|
56
|
+
continue;
|
|
57
|
+
const collection = doc.collection(objType);
|
|
58
|
+
const count = collection.size;
|
|
59
|
+
if (count <= 1)
|
|
60
|
+
continue;
|
|
61
|
+
const first = collection.first;
|
|
62
|
+
errors.push({
|
|
63
|
+
severity: Severity.ERROR,
|
|
64
|
+
objType,
|
|
65
|
+
objName: first !== undefined && first.name !== '' ? first.name : objType,
|
|
66
|
+
field: undefined,
|
|
67
|
+
message: `Singleton type '${objType}' has ${count} instances (maximum 1 allowed)`,
|
|
68
|
+
code: 'E010',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const objType of typesToCheck) {
|
|
73
|
+
for (const obj of doc.collection(objType)) {
|
|
74
|
+
for (const finding of findingsFor(obj, schema, checks)) {
|
|
75
|
+
if (finding.severity === Severity.ERROR)
|
|
76
|
+
errors.push(finding);
|
|
77
|
+
else if (finding.severity === Severity.WARNING)
|
|
78
|
+
warnings.push(finding);
|
|
79
|
+
else
|
|
80
|
+
info.push(finding);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (checkReferences) {
|
|
85
|
+
const unpopulated = unpopulatedLists(schema);
|
|
86
|
+
const dangling = doc.danglingReferences();
|
|
87
|
+
// Both sets cost a full document walk, so they are built once, and only
|
|
88
|
+
// when there is something to test them against.
|
|
89
|
+
const names = dangling.length > 0 ? declaredNames(doc.objects()) : undefined;
|
|
90
|
+
for (const edge of dangling) {
|
|
91
|
+
// A field pointing only into lists nothing can ever contribute to is not
|
|
92
|
+
// naming an object at all, so its value cannot be dangling.
|
|
93
|
+
if (pointsOnlyIntoUnpopulated(schema, edge.from.typeName, edge.field, unpopulated))
|
|
94
|
+
continue;
|
|
95
|
+
// A name EnergyPlus mints itself is not dangling either, even though no
|
|
96
|
+
// object declares it: one instance of a ZoneList assignment expanded per
|
|
97
|
+
// member, or the leftover space of a partly covered zone.
|
|
98
|
+
if (names !== undefined && isZoneListExpansion(edge.target, names))
|
|
99
|
+
continue;
|
|
100
|
+
if (names !== undefined && isRemainderSpace(edge.target, names))
|
|
101
|
+
continue;
|
|
102
|
+
errors.push({
|
|
103
|
+
severity: Severity.ERROR,
|
|
104
|
+
objType: edge.from.typeName,
|
|
105
|
+
objName: edge.from.name,
|
|
106
|
+
field: edge.field,
|
|
107
|
+
message: `Reference to non-existent object '${edge.target}'`,
|
|
108
|
+
code: 'E009',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
errors,
|
|
114
|
+
warnings,
|
|
115
|
+
info,
|
|
116
|
+
isValid: errors.length === 0,
|
|
117
|
+
totalIssues: errors.length + warnings.length + info.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validate one object, attached or detached.
|
|
122
|
+
*
|
|
123
|
+
* Useful right after building an object, before it goes anywhere near a
|
|
124
|
+
* document. The schema is passed explicitly rather than taken from the object
|
|
125
|
+
* so that "does this object hold up under another EnergyPlus version?" is
|
|
126
|
+
* askable; a type the schema does not define is reported as `W002` rather than
|
|
127
|
+
* throwing.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const material = doc.add('Material', 'Gypsum', { roughness: 'Smooth' });
|
|
132
|
+
* const findings = validateObject(material, doc.schema);
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function validateObject(obj, schema, options = {}) {
|
|
136
|
+
return findingsFor(obj, schema, {
|
|
137
|
+
required: options.checkRequired ?? true,
|
|
138
|
+
types: options.checkTypes ?? true,
|
|
139
|
+
ranges: options.checkRanges ?? true,
|
|
140
|
+
unknown: options.checkUnknown ?? true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// The per-object walk
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
function findingsFor(obj, schema, checks) {
|
|
147
|
+
const findings = [];
|
|
148
|
+
const objType = obj.typeName;
|
|
149
|
+
const objName = obj.name;
|
|
150
|
+
const type = schema.get(objType);
|
|
151
|
+
if (type === undefined) {
|
|
152
|
+
findings.push({
|
|
153
|
+
severity: Severity.WARNING,
|
|
154
|
+
objType,
|
|
155
|
+
objName,
|
|
156
|
+
field: undefined,
|
|
157
|
+
message: `Unknown object type '${objType}'`,
|
|
158
|
+
code: 'W002',
|
|
159
|
+
});
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
162
|
+
if (checks.required) {
|
|
163
|
+
for (const field of type.r ?? []) {
|
|
164
|
+
const value = obj[DATA][field];
|
|
165
|
+
if (value === undefined || value === '') {
|
|
166
|
+
findings.push({
|
|
167
|
+
severity: Severity.ERROR,
|
|
168
|
+
objType,
|
|
169
|
+
objName,
|
|
170
|
+
field,
|
|
171
|
+
message: `Required field '${field}' is missing`,
|
|
172
|
+
code: 'E001',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Read the backing store rather than `toJSON()`: this is the object's own
|
|
178
|
+
// insertion order, which is what the Python original walks, and it avoids
|
|
179
|
+
// copying every extensible group just to look at it.
|
|
180
|
+
for (const [field, value] of Object.entries(obj[DATA])) {
|
|
181
|
+
if (value === undefined || value === '')
|
|
182
|
+
continue;
|
|
183
|
+
const fieldSchema = type.p[field];
|
|
184
|
+
if (fieldSchema === undefined) {
|
|
185
|
+
// On an extensible type the field may belong to the repeat group rather
|
|
186
|
+
// than the fixed list, so an unrecognized name there is not evidence of
|
|
187
|
+
// anything.
|
|
188
|
+
if (checks.unknown && type.x === undefined) {
|
|
189
|
+
findings.push({
|
|
190
|
+
severity: Severity.WARNING,
|
|
191
|
+
objType,
|
|
192
|
+
objName,
|
|
193
|
+
field,
|
|
194
|
+
message: `Unknown field '${field}'`,
|
|
195
|
+
code: 'W003',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// One finding per field, whatever the field is. A value that is both the
|
|
201
|
+
// wrong type and out of range is one defect described twice, and reporting
|
|
202
|
+
// it twice makes a caller fix it twice.
|
|
203
|
+
//
|
|
204
|
+
// An `anyOf` field is evaluated as a single unit, because its type, its enum
|
|
205
|
+
// and its bounds all belong to a branch and cannot be checked in separate
|
|
206
|
+
// passes: a value satisfying one branch's type and another branch's absence
|
|
207
|
+
// of bounds satisfies neither branch. Which switch silences the result is
|
|
208
|
+
// therefore decided after the fact, from the code the evaluation produced.
|
|
209
|
+
if (fieldSchema.auto === 1) {
|
|
210
|
+
const failure = anyOfFailure(value, fieldSchema);
|
|
211
|
+
if (failure === undefined)
|
|
212
|
+
continue;
|
|
213
|
+
if (RANGE_CODES.has(failure.code) ? checks.ranges : checks.types) {
|
|
214
|
+
findings.push(error(obj, field, failure));
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// A plain field is three ordered checks — type, then enum, then bounds —
|
|
219
|
+
// under the two switches that have always gated them.
|
|
220
|
+
const failure = (checks.types ? plainFailure(value, fieldSchema) : undefined) ??
|
|
221
|
+
(checks.ranges && typeof value === 'number'
|
|
222
|
+
? rangeFailures(value, fieldSchema)[0]
|
|
223
|
+
: undefined);
|
|
224
|
+
if (failure !== undefined)
|
|
225
|
+
findings.push(error(obj, field, failure));
|
|
226
|
+
}
|
|
227
|
+
return findings;
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// References
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
/** Per-schema cache: the answer depends only on the schema, and costs a full walk. */
|
|
233
|
+
const UNPOPULATED_BY_SCHEMA = new WeakMap();
|
|
234
|
+
/**
|
|
235
|
+
* Reference lists that are pointed INTO and never contributed TO.
|
|
236
|
+
*
|
|
237
|
+
* `object_list` says a field points into a list; `reference` says a name
|
|
238
|
+
* contributes to one. Four lists in 26.1.0 — `validBranchEquipmentTypes`,
|
|
239
|
+
* `validCondenserEquipmentTypes`, `validOASysEquipmentTypes` and
|
|
240
|
+
* `validPlantEquipmentTypes` — are only ever pointed into. They enumerate object
|
|
241
|
+
* TYPE names rather than object names, so `component_object_type` holding
|
|
242
|
+
* `Pipe:Adiabatic` is a correct value that no object in any model will ever
|
|
243
|
+
* declare. Reporting those as dangling is 19783 false findings over the 760
|
|
244
|
+
* EnergyPlus example files, one for nearly every branch component in every model.
|
|
245
|
+
*
|
|
246
|
+
* Derived from the schema rather than listed here: the set is version-dependent
|
|
247
|
+
* (older versions carry a couple more), and a hardcoded list would go stale
|
|
248
|
+
* silently on the next EnergyPlus release.
|
|
249
|
+
*/
|
|
250
|
+
function unpopulatedLists(schema) {
|
|
251
|
+
const cached = UNPOPULATED_BY_SCHEMA.get(schema);
|
|
252
|
+
if (cached !== undefined)
|
|
253
|
+
return cached;
|
|
254
|
+
const pointedInto = new Set();
|
|
255
|
+
const contributedTo = new Set();
|
|
256
|
+
for (const typeName of schema.typeNames) {
|
|
257
|
+
const type = schema.get(typeName);
|
|
258
|
+
if (type === undefined)
|
|
259
|
+
continue;
|
|
260
|
+
for (const list of type.nref ?? [])
|
|
261
|
+
contributedTo.add(list);
|
|
262
|
+
for (const props of [type.p, type.x?.p]) {
|
|
263
|
+
for (const fieldSchema of Object.values(props ?? {})) {
|
|
264
|
+
for (const list of fieldSchema.ol ?? [])
|
|
265
|
+
pointedInto.add(list);
|
|
266
|
+
for (const list of fieldSchema.ref ?? [])
|
|
267
|
+
contributedTo.add(list);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const unpopulated = new Set();
|
|
272
|
+
for (const list of pointedInto)
|
|
273
|
+
if (!contributedTo.has(list))
|
|
274
|
+
unpopulated.add(list);
|
|
275
|
+
UNPOPULATED_BY_SCHEMA.set(schema, unpopulated);
|
|
276
|
+
return unpopulated;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Whether every list this field points into is one nothing can populate.
|
|
280
|
+
*
|
|
281
|
+
* Every, not any: a field naming several lists is dangling as long as one of
|
|
282
|
+
* them is a list some object could have contributed to. A field the schema does
|
|
283
|
+
* not describe keeps its finding, because silence there would be a guess.
|
|
284
|
+
*/
|
|
285
|
+
function pointsOnlyIntoUnpopulated(schema, typeName, field, unpopulated) {
|
|
286
|
+
if (unpopulated.size === 0)
|
|
287
|
+
return false;
|
|
288
|
+
const type = schema.get(typeName);
|
|
289
|
+
// Extensible reference fields (`Branch.component_object_type`) are defined on
|
|
290
|
+
// the repeat group, not alongside the fixed fields.
|
|
291
|
+
const fieldSchema = type?.p[field] ?? type?.x?.p[field];
|
|
292
|
+
const lists = fieldSchema?.ol;
|
|
293
|
+
if (lists === undefined || lists.length === 0)
|
|
294
|
+
return false;
|
|
295
|
+
return lists.every((list) => unpopulated.has(list));
|
|
296
|
+
}
|
|
297
|
+
/** Collect every name set in a single document walk. */
|
|
298
|
+
function declaredNames(objects) {
|
|
299
|
+
const all = new Set();
|
|
300
|
+
const zonesAndSpaces = new Set();
|
|
301
|
+
const zones = new Set();
|
|
302
|
+
for (const obj of objects) {
|
|
303
|
+
// Same reasoning as `danglingReferences()`: anonymous types declare the
|
|
304
|
+
// name others reference from an ordinary field, so `name` alone is short.
|
|
305
|
+
for (const declared of obj.declaredNames()) {
|
|
306
|
+
const lower = declared.toLowerCase();
|
|
307
|
+
all.add(lower);
|
|
308
|
+
if (obj.typeName === 'Zone' || obj.typeName === 'Space')
|
|
309
|
+
zonesAndSpaces.add(lower);
|
|
310
|
+
if (obj.typeName === 'Zone')
|
|
311
|
+
zones.add(lower);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { all, zonesAndSpaces, zones };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Whether a target names an object EnergyPlus creates by expanding a ZoneList.
|
|
318
|
+
*
|
|
319
|
+
* An object assigned to a `ZoneList` (or a `SpaceList`) is expanded by
|
|
320
|
+
* EnergyPlus into one instance per member, named `<member name>` + a single
|
|
321
|
+
* space + `<object name>`, and other objects legitimately reference those
|
|
322
|
+
* expanded names. `DemandManager:ElectricEquipment.electric_equipment_name`
|
|
323
|
+
* documents the convention in the schema itself: "if ZoneList option is used on
|
|
324
|
+
* the ElectricEquipment object, a single equipment object from that assignment
|
|
325
|
+
* can be selected by entering <Zone Name><space><Global ElectricEquipment
|
|
326
|
+
* Object Name>". No object in the file declares that name, so the reference
|
|
327
|
+
* looks dangling and is not.
|
|
328
|
+
*
|
|
329
|
+
* Every space is tried, not only the first: zone names and object names both
|
|
330
|
+
* routinely contain spaces, so the split point cannot be assumed.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately an approximation. It does not verify that the suffix object is
|
|
333
|
+
* actually assigned to a ZoneList containing the prefix zone; doing so needs
|
|
334
|
+
* ZoneList membership resolution that this library and its Python counterpart
|
|
335
|
+
* do not share today. The approximation can only ever suppress a finding, never
|
|
336
|
+
* invent one, so it cannot produce a false NEGATIVE on a valid model, which is
|
|
337
|
+
* the property that matters here. This is a choice, not an oversight.
|
|
338
|
+
*/
|
|
339
|
+
function isZoneListExpansion(target, names) {
|
|
340
|
+
if (names.zonesAndSpaces.size === 0)
|
|
341
|
+
return false;
|
|
342
|
+
const lower = target.toLowerCase();
|
|
343
|
+
for (let at = lower.indexOf(' '); at !== -1; at = lower.indexOf(' ', at + 1)) {
|
|
344
|
+
if (!names.zonesAndSpaces.has(lower.slice(0, at)))
|
|
345
|
+
continue;
|
|
346
|
+
if (names.all.has(lower.slice(at + 1)))
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
/** The name EnergyPlus gives a zone's leftover space is its zone name plus this. */
|
|
352
|
+
const REMAINDER_SUFFIX = '-remainder';
|
|
353
|
+
/**
|
|
354
|
+
* Whether a target names the implicit remainder space of a declared zone.
|
|
355
|
+
*
|
|
356
|
+
* A zone that carries `Space` objects covering only part of it gets one more
|
|
357
|
+
* space from EnergyPlus, holding whatever is left over and named
|
|
358
|
+
* `<Zone Name>-Remainder`. Nothing declares that name, and objects reference it
|
|
359
|
+
* like any other space. `5ZoneAirCooledWithSpacesHVAC.idf` names
|
|
360
|
+
* `Zone 5-Remainder` twelve times: `Zone 5` is declared and carries the spaces
|
|
361
|
+
* `Space 5 Office` and `Space 5 Conference`, while `Zone 5-Remainder` is
|
|
362
|
+
* declared nowhere.
|
|
363
|
+
*
|
|
364
|
+
* Kept separate from `isZoneListExpansion` rather than folded into it: the two
|
|
365
|
+
* joins differ, a hyphen against a space, and only a `Zone` can prefix this one
|
|
366
|
+
* where a `Space` may prefix that one. Merging them would obscure both.
|
|
367
|
+
*
|
|
368
|
+
* An approximation in the same spirit as the ZoneList rule: it does not verify
|
|
369
|
+
* that the zone's declared spaces actually leave a remainder. It can only
|
|
370
|
+
* suppress a finding, never invent one, so no valid model gains a false
|
|
371
|
+
* NEGATIVE from it. A choice, not an oversight.
|
|
372
|
+
*/
|
|
373
|
+
function isRemainderSpace(target, names) {
|
|
374
|
+
if (names.zones.size === 0)
|
|
375
|
+
return false;
|
|
376
|
+
const lower = target.toLowerCase();
|
|
377
|
+
if (!lower.endsWith(REMAINDER_SUFFIX))
|
|
378
|
+
return false;
|
|
379
|
+
return names.zones.has(lower.slice(0, lower.length - REMAINDER_SUFFIX.length));
|
|
380
|
+
}
|
|
381
|
+
/** Codes that belong to the range check rather than the type check. */
|
|
382
|
+
const RANGE_CODES = new Set(['E005', 'E006', 'E007', 'E008']);
|
|
383
|
+
function error(obj, field, failure) {
|
|
384
|
+
return {
|
|
385
|
+
severity: Severity.ERROR,
|
|
386
|
+
objType: obj.typeName,
|
|
387
|
+
objName: obj.name,
|
|
388
|
+
field,
|
|
389
|
+
message: failure.message,
|
|
390
|
+
code: failure.code,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Evaluate a field the schema declares as `anyOf: [{number}, {string}]`.
|
|
395
|
+
*
|
|
396
|
+
* Ordinary JSON Schema `anyOf`: the value is valid when it satisfies at least
|
|
397
|
+
* one branch *completely* — that branch's type, its enum if it has one, and its
|
|
398
|
+
* bounds if it has any. Checking the type against one branch and the constraints
|
|
399
|
+
* against another is exactly the defect this replaces; a value could satisfy the
|
|
400
|
+
* number branch's type and the string branch's absence of bounds and be accepted
|
|
401
|
+
* having satisfied neither.
|
|
402
|
+
*
|
|
403
|
+
* The branches are reconstructed from the slim record: the numeric branch is
|
|
404
|
+
* `t` plus `e`, `min`, `max`, `xmin` and `xmax`, all of which the bundle hoists
|
|
405
|
+
* off it, and the string branch is `se`, which is absent exactly when the branch
|
|
406
|
+
* carried no enum and any string is legal.
|
|
407
|
+
*
|
|
408
|
+
* At most one finding, and it names the failure rather than the shape: an
|
|
409
|
+
* out-of-range number reported as "not a number" tells the caller nothing.
|
|
410
|
+
*/
|
|
411
|
+
function anyOfFailure(value, fieldSchema) {
|
|
412
|
+
// 1. The branches whose TYPE the value satisfies. A value is a number or a
|
|
413
|
+
// string, never both, so at most one of these holds; the code is written
|
|
414
|
+
// as a set anyway because the rule is.
|
|
415
|
+
const numeric = matchesKind(value, fieldSchema.t);
|
|
416
|
+
const stringly = typeof value === 'string';
|
|
417
|
+
// 2. No branch matched on type: an array where a number or a string belongs.
|
|
418
|
+
if (!numeric && !stringly) {
|
|
419
|
+
return { code: 'E002', message: `Value '${render(value)}' does not match any valid type` };
|
|
420
|
+
}
|
|
421
|
+
// 3. A branch satisfied whole makes the value valid, whatever the other says.
|
|
422
|
+
const numericFailure = numeric ? numericBranchFailure(value, fieldSchema) : undefined;
|
|
423
|
+
if (numeric && numericFailure === undefined)
|
|
424
|
+
return undefined;
|
|
425
|
+
const stringFailure = stringly ? stringBranchFailure(value, fieldSchema) : undefined;
|
|
426
|
+
if (stringly && stringFailure === undefined)
|
|
427
|
+
return undefined;
|
|
428
|
+
// 4. Matched on type, failed on a constraint. Report the first branch in
|
|
429
|
+
// declaration order, which is the numeric one in every `anyOf` in all 17
|
|
430
|
+
// bundled versions.
|
|
431
|
+
return numericFailure ?? stringFailure;
|
|
432
|
+
}
|
|
433
|
+
/** The numeric branch: its enum, then its bounds. `undefined` when satisfied. */
|
|
434
|
+
function numericBranchFailure(value, fieldSchema) {
|
|
435
|
+
const allowed = fieldSchema.e;
|
|
436
|
+
if (allowed !== undefined && !satisfiesEnum(allowed, value)) {
|
|
437
|
+
return enumFailure(allowed, value);
|
|
438
|
+
}
|
|
439
|
+
return rangeFailures(value, fieldSchema)[0];
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The string branch: its enum, if it has one.
|
|
443
|
+
*
|
|
444
|
+
* No `se` means the branch declared no enum and any string satisfies it, which
|
|
445
|
+
* is the shape of 646 fields including `Schedule:Compact`'s extensible `field`.
|
|
446
|
+
* `se` is never inferred: the sentinel is `Autosize` on some fields and
|
|
447
|
+
* `Autocalculate` on others, and accepting either everywhere would accept a
|
|
448
|
+
* value EnergyPlus rejects.
|
|
449
|
+
*/
|
|
450
|
+
function stringBranchFailure(value, fieldSchema) {
|
|
451
|
+
const allowed = fieldSchema.se;
|
|
452
|
+
if (allowed === undefined || satisfiesEnum(allowed, value))
|
|
453
|
+
return undefined;
|
|
454
|
+
return enumFailure(allowed, value);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* A field the schema declares with one type: its type, then its enum.
|
|
458
|
+
*
|
|
459
|
+
* The bounds are the caller's business, because they answer to a different
|
|
460
|
+
* switch. Stopping at the first failure is what keeps a wrong-typed value from
|
|
461
|
+
* also being reported against a choice list it was never going to satisfy.
|
|
462
|
+
*/
|
|
463
|
+
function plainFailure(value, fieldSchema) {
|
|
464
|
+
if (!matchesKind(value, fieldSchema.t)) {
|
|
465
|
+
return {
|
|
466
|
+
code: 'E003',
|
|
467
|
+
message: `Expected ${jsonTypeName(fieldSchema.t)}, got ${describeType(value)}`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const allowed = fieldSchema.e;
|
|
471
|
+
if (allowed === undefined || satisfiesEnum(allowed, value))
|
|
472
|
+
return undefined;
|
|
473
|
+
return enumFailure(allowed, value);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Enum membership.
|
|
477
|
+
*
|
|
478
|
+
* Exact match first, then case-insensitively for strings, which is how
|
|
479
|
+
* EnergyPlus reads a choice field and how the Python original checks it. Numbers
|
|
480
|
+
* compare by value: 68 fields across the versions state their choices
|
|
481
|
+
* numerically, and `3` is not one of `[0, 1]`.
|
|
482
|
+
*/
|
|
483
|
+
function satisfiesEnum(allowed, value) {
|
|
484
|
+
return allowed.some((choice) => choice === value ||
|
|
485
|
+
(typeof choice === 'string' &&
|
|
486
|
+
typeof value === 'string' &&
|
|
487
|
+
choice.toLowerCase() === value.toLowerCase()));
|
|
488
|
+
}
|
|
489
|
+
function enumFailure(allowed, value) {
|
|
490
|
+
return {
|
|
491
|
+
code: 'E004',
|
|
492
|
+
message: `Value '${render(value)}' not in allowed values: [${allowed
|
|
493
|
+
.map((choice) => `'${choice}'`)
|
|
494
|
+
.join(', ')}]`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Whether a stored value satisfies the schema's storage class for its field. */
|
|
498
|
+
function matchesKind(value, kind) {
|
|
499
|
+
switch (kind) {
|
|
500
|
+
case 'n':
|
|
501
|
+
return typeof value === 'number';
|
|
502
|
+
case 'i':
|
|
503
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
504
|
+
case 'a':
|
|
505
|
+
return typeof value === 'string';
|
|
506
|
+
case 'arr':
|
|
507
|
+
return Array.isArray(value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** The epJSON `type` keyword the storage class stands for, for messages. */
|
|
511
|
+
function jsonTypeName(kind) {
|
|
512
|
+
switch (kind) {
|
|
513
|
+
case 'n':
|
|
514
|
+
return 'number';
|
|
515
|
+
case 'i':
|
|
516
|
+
return 'integer';
|
|
517
|
+
case 'a':
|
|
518
|
+
return 'string';
|
|
519
|
+
case 'arr':
|
|
520
|
+
return 'array';
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function describeType(value) {
|
|
524
|
+
return Array.isArray(value) ? 'array' : typeof value;
|
|
525
|
+
}
|
|
526
|
+
function render(value) {
|
|
527
|
+
return Array.isArray(value) ? JSON.stringify(value) : String(value);
|
|
528
|
+
}
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// Range
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
/**
|
|
533
|
+
* Every bound the value breaks, in `minimum`, `exclusiveMinimum`, `maximum`,
|
|
534
|
+
* `exclusiveMaximum` order.
|
|
535
|
+
*
|
|
536
|
+
* `xmin`/`xmax` arrive in either of the two JSON Schema dialects the bundled
|
|
537
|
+
* schemas ship. From 9.6.0 they are draft-06+ and carry the bound itself; for
|
|
538
|
+
* 8.9.0 through 9.5.0 they are draft-04 and are the boolean `true`, qualifying
|
|
539
|
+
* the sibling `minimum`/`maximum`. Reading `true` as a number would treat it as
|
|
540
|
+
* 1 and reject every value at or below 1 in a positive-bounded field, so this
|
|
541
|
+
* branches on the type of the value and never on the version.
|
|
542
|
+
*/
|
|
543
|
+
function rangeFailures(value, bounds) {
|
|
544
|
+
const { min, max, xmin, xmax } = bounds;
|
|
545
|
+
const out = [];
|
|
546
|
+
if (min !== undefined) {
|
|
547
|
+
// draft-04: the sibling flag makes `min` exclusive.
|
|
548
|
+
if (xmin === true) {
|
|
549
|
+
if (value <= min)
|
|
550
|
+
out.push({ code: 'E006', message: `Value ${value} must be greater than ${min}` });
|
|
551
|
+
}
|
|
552
|
+
else if (value < min) {
|
|
553
|
+
out.push({ code: 'E005', message: `Value ${value} is below minimum ${min}` });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// draft-06+: the key carries the bound itself.
|
|
557
|
+
if (typeof xmin === 'number' && value <= xmin) {
|
|
558
|
+
out.push({ code: 'E006', message: `Value ${value} must be greater than ${xmin}` });
|
|
559
|
+
}
|
|
560
|
+
if (max !== undefined) {
|
|
561
|
+
if (xmax === true) {
|
|
562
|
+
if (value >= max)
|
|
563
|
+
out.push({ code: 'E008', message: `Value ${value} must be less than ${max}` });
|
|
564
|
+
}
|
|
565
|
+
else if (value > max) {
|
|
566
|
+
out.push({ code: 'E007', message: `Value ${value} is above maximum ${max}` });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (typeof xmax === 'number' && value >= xmax) {
|
|
570
|
+
out.push({ code: 'E008', message: `Value ${value} must be less than ${xmax}` });
|
|
571
|
+
}
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
//# sourceMappingURL=validate.js.map
|