circuitjson-toolkit 1.0.3 → 1.0.16
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/AGENTS.md +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
2
|
+
|
|
3
|
+
const KNOWN_ELEMENT_TYPES = new Set([
|
|
4
|
+
'cad_component',
|
|
5
|
+
'circuit_json_footprint_load_error',
|
|
6
|
+
'external_footprint_load_error',
|
|
7
|
+
'pcb_autorouting_error',
|
|
8
|
+
'pcb_board',
|
|
9
|
+
'pcb_breakout_point',
|
|
10
|
+
'pcb_component',
|
|
11
|
+
'pcb_component_invalid_layer_error',
|
|
12
|
+
'pcb_component_not_on_board_edge_error',
|
|
13
|
+
'pcb_component_outside_board_error',
|
|
14
|
+
'pcb_connector_not_in_accessible_orientation_warning',
|
|
15
|
+
'pcb_copper_pour',
|
|
16
|
+
'pcb_copper_text',
|
|
17
|
+
'pcb_courtyard',
|
|
18
|
+
'pcb_courtyard_circle',
|
|
19
|
+
'pcb_courtyard_line',
|
|
20
|
+
'pcb_courtyard_outline',
|
|
21
|
+
'pcb_courtyard_path',
|
|
22
|
+
'pcb_courtyard_overlap_error',
|
|
23
|
+
'pcb_courtyard_pill',
|
|
24
|
+
'pcb_courtyard_polygon',
|
|
25
|
+
'pcb_courtyard_rect',
|
|
26
|
+
'pcb_cutout',
|
|
27
|
+
'pcb_fabrication_note_dimension',
|
|
28
|
+
'pcb_fabrication_note_path',
|
|
29
|
+
'pcb_fabrication_note_rect',
|
|
30
|
+
'pcb_fabrication_note_text',
|
|
31
|
+
'pcb_footprint_overlap_error',
|
|
32
|
+
'pcb_ground_plane',
|
|
33
|
+
'pcb_ground_plane_region',
|
|
34
|
+
'pcb_group',
|
|
35
|
+
'pcb_hole',
|
|
36
|
+
'pcb_keepout',
|
|
37
|
+
'pcb_manual_edit_conflict_warning',
|
|
38
|
+
'pcb_missing_footprint_error',
|
|
39
|
+
'pcb_net',
|
|
40
|
+
'pcb_note_dimension',
|
|
41
|
+
'pcb_note_line',
|
|
42
|
+
'pcb_note_path',
|
|
43
|
+
'pcb_note_rect',
|
|
44
|
+
'pcb_note_text',
|
|
45
|
+
'pcb_pad_pad_clearance_error',
|
|
46
|
+
'pcb_pad_trace_clearance_error',
|
|
47
|
+
'pcb_panel',
|
|
48
|
+
'pcb_panelization_placement_error',
|
|
49
|
+
'pcb_placement_error',
|
|
50
|
+
'pcb_plated_hole',
|
|
51
|
+
'pcb_port',
|
|
52
|
+
'pcb_port_not_connected_error',
|
|
53
|
+
'pcb_port_not_matched_error',
|
|
54
|
+
'pcb_silkscreen_circle',
|
|
55
|
+
'pcb_silkscreen_graphic',
|
|
56
|
+
'pcb_silkscreen_line',
|
|
57
|
+
'pcb_silkscreen_oval',
|
|
58
|
+
'pcb_silkscreen_path',
|
|
59
|
+
'pcb_silkscreen_pill',
|
|
60
|
+
'pcb_silkscreen_rect',
|
|
61
|
+
'pcb_silkscreen_text',
|
|
62
|
+
'pcb_smtpad',
|
|
63
|
+
'pcb_solder_paste',
|
|
64
|
+
'pcb_text',
|
|
65
|
+
'pcb_thermal_spoke',
|
|
66
|
+
'pcb_trace',
|
|
67
|
+
'pcb_trace_error',
|
|
68
|
+
'pcb_trace_hint',
|
|
69
|
+
'pcb_trace_missing_error',
|
|
70
|
+
'pcb_trace_warning',
|
|
71
|
+
'pcb_via',
|
|
72
|
+
'pcb_via_clearance_error',
|
|
73
|
+
'pcb_via_trace_clearance_error',
|
|
74
|
+
'schematic_arc',
|
|
75
|
+
'schematic_box',
|
|
76
|
+
'schematic_circle',
|
|
77
|
+
'schematic_component',
|
|
78
|
+
'schematic_debug_object',
|
|
79
|
+
'schematic_error',
|
|
80
|
+
'schematic_group',
|
|
81
|
+
'schematic_layout_error',
|
|
82
|
+
'schematic_line',
|
|
83
|
+
'schematic_manual_edit_conflict_warning',
|
|
84
|
+
'schematic_net_label',
|
|
85
|
+
'schematic_path',
|
|
86
|
+
'schematic_port',
|
|
87
|
+
'schematic_rect',
|
|
88
|
+
'schematic_sheet',
|
|
89
|
+
'schematic_symbol',
|
|
90
|
+
'schematic_table',
|
|
91
|
+
'schematic_table_cell',
|
|
92
|
+
'schematic_text',
|
|
93
|
+
'schematic_trace',
|
|
94
|
+
'schematic_voltage_probe',
|
|
95
|
+
'simulation_current_probe',
|
|
96
|
+
'simulation_current_source',
|
|
97
|
+
'simulation_experiment',
|
|
98
|
+
'simulation_op_amp',
|
|
99
|
+
'simulation_oscilloscope_trace',
|
|
100
|
+
'simulation_spice_subcircuit',
|
|
101
|
+
'simulation_switch',
|
|
102
|
+
'simulation_transient_current_graph',
|
|
103
|
+
'simulation_transient_voltage_graph',
|
|
104
|
+
'simulation_unknown_experiment_error',
|
|
105
|
+
'simulation_voltage_probe',
|
|
106
|
+
'simulation_voltage_source',
|
|
107
|
+
'source_ambiguous_port_reference',
|
|
108
|
+
'source_board',
|
|
109
|
+
'source_component',
|
|
110
|
+
'source_component_internal_connection',
|
|
111
|
+
'source_component_misconfigured_error',
|
|
112
|
+
'source_component_pins_underspecified_warning',
|
|
113
|
+
'source_failed_to_create_component_error',
|
|
114
|
+
'source_group',
|
|
115
|
+
'source_i2c_misconfigured_error',
|
|
116
|
+
'source_invalid_component_property_error',
|
|
117
|
+
'source_manually_placed_via',
|
|
118
|
+
'source_missing_manufacturer_part_number_warning',
|
|
119
|
+
'source_missing_property_error',
|
|
120
|
+
'source_net',
|
|
121
|
+
'source_no_ground_pin_defined_warning',
|
|
122
|
+
'source_no_power_pin_defined_warning',
|
|
123
|
+
'source_pcb_ground_plane',
|
|
124
|
+
'source_pin_missing_trace_warning',
|
|
125
|
+
'source_pin_must_be_connected_error',
|
|
126
|
+
'source_port',
|
|
127
|
+
'source_project_metadata',
|
|
128
|
+
'source_property_ignored_warning',
|
|
129
|
+
'source_trace',
|
|
130
|
+
'source_trace_not_connected_error',
|
|
131
|
+
'supplier_footprint_mismatch_warning',
|
|
132
|
+
'unknown_error_finding_part'
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const ID_FIELD_EXCEPTIONS = new Set([
|
|
136
|
+
'pcb_autorouting_error',
|
|
137
|
+
'pcb_courtyard_overlap_error',
|
|
138
|
+
'pcb_footprint_overlap_error',
|
|
139
|
+
'pcb_port_not_matched_error',
|
|
140
|
+
'pcb_via_clearance_error',
|
|
141
|
+
'schematic_box',
|
|
142
|
+
'schematic_debug_object',
|
|
143
|
+
'source_project_metadata'
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
const SOURCE_COMPONENT_FTYPES = new Set([
|
|
147
|
+
'interconnect',
|
|
148
|
+
'simple_ammeter',
|
|
149
|
+
'simple_battery',
|
|
150
|
+
'simple_capacitor',
|
|
151
|
+
'simple_chip',
|
|
152
|
+
'simple_connector',
|
|
153
|
+
'simple_crystal',
|
|
154
|
+
'simple_current_source',
|
|
155
|
+
'simple_diode',
|
|
156
|
+
'simple_fiducial',
|
|
157
|
+
'simple_fuse',
|
|
158
|
+
'simple_ground',
|
|
159
|
+
'simple_inductor',
|
|
160
|
+
'simple_led',
|
|
161
|
+
'simple_mosfet',
|
|
162
|
+
'simple_op_amp',
|
|
163
|
+
'simple_pin_header',
|
|
164
|
+
'simple_pinout',
|
|
165
|
+
'simple_potentiometer',
|
|
166
|
+
'simple_power_source',
|
|
167
|
+
'simple_push_button',
|
|
168
|
+
'simple_resistor',
|
|
169
|
+
'simple_resonator',
|
|
170
|
+
'simple_switch',
|
|
171
|
+
'simple_test_point',
|
|
172
|
+
'simple_transistor',
|
|
173
|
+
'simple_voltage_probe',
|
|
174
|
+
'simple_voltage_source'
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
const LAYERS = new Set(
|
|
178
|
+
'top bottom inner1 inner2 inner3 inner4 inner5 inner6'.split(' ')
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const SMT_PAD_SHAPES = new Set([
|
|
182
|
+
'circle',
|
|
183
|
+
'rect',
|
|
184
|
+
'rotated_rect',
|
|
185
|
+
'rotated_pill',
|
|
186
|
+
'pill',
|
|
187
|
+
'polygon',
|
|
188
|
+
'rounded_rect'
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
const PCB_HOLE_SHAPES = new Set([
|
|
192
|
+
'circle',
|
|
193
|
+
'circle_or_square',
|
|
194
|
+
'oval',
|
|
195
|
+
'pill',
|
|
196
|
+
'rect',
|
|
197
|
+
'rotated_pill',
|
|
198
|
+
'round'
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
const PCB_PLATED_HOLE_SHAPES = new Set([
|
|
202
|
+
'circle',
|
|
203
|
+
'circular_hole_with_rect_pad',
|
|
204
|
+
'hole_with_polygon_pad',
|
|
205
|
+
'oval',
|
|
206
|
+
'pill',
|
|
207
|
+
'pill_hole_with_rect_pad',
|
|
208
|
+
'rotated_pill_hole_with_rect_pad'
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
const PCB_PLATED_HOLE_HOLE_SHAPES = new Set([
|
|
212
|
+
'circle',
|
|
213
|
+
'oval',
|
|
214
|
+
'pill',
|
|
215
|
+
'rotated_pill'
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
const PCB_SOLDER_PASTE_SHAPES = new Set([
|
|
219
|
+
'circle',
|
|
220
|
+
'oval',
|
|
221
|
+
'pill',
|
|
222
|
+
'rect',
|
|
223
|
+
'rotated_rect'
|
|
224
|
+
])
|
|
225
|
+
|
|
226
|
+
const PCB_BOARD_SHAPES = new Set(['polygon', 'rect'])
|
|
227
|
+
|
|
228
|
+
const PCB_CUTOUT_SHAPES = new Set(['circle', 'path', 'polygon', 'rect'])
|
|
229
|
+
|
|
230
|
+
const PCB_COPPER_POUR_SHAPES = new Set(['brep', 'polygon', 'rect'])
|
|
231
|
+
|
|
232
|
+
const SIMULATION_SOURCE_KINDS = new Set(['ac', 'dc'])
|
|
233
|
+
|
|
234
|
+
const SIMULATION_WAVE_SHAPES = new Set(
|
|
235
|
+
'sawtooth sinewave square triangle'.split(' ')
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
const SIMULATION_EXPERIMENT_METHODS = new Set(['gear', 'trap'])
|
|
239
|
+
|
|
240
|
+
const VARIANT_SETS = {
|
|
241
|
+
sourceComponentFtypes: SOURCE_COMPONENT_FTYPES,
|
|
242
|
+
pcbBoardShapes: PCB_BOARD_SHAPES,
|
|
243
|
+
pcbSmtPadShapes: SMT_PAD_SHAPES,
|
|
244
|
+
pcbHoleShapes: PCB_HOLE_SHAPES,
|
|
245
|
+
pcbPlatedHoleShapes: PCB_PLATED_HOLE_SHAPES,
|
|
246
|
+
pcbPlatedHoleHoleShapes: PCB_PLATED_HOLE_HOLE_SHAPES,
|
|
247
|
+
pcbSolderPasteShapes: PCB_SOLDER_PASTE_SHAPES,
|
|
248
|
+
pcbCutoutShapes: PCB_CUTOUT_SHAPES,
|
|
249
|
+
pcbCopperPourShapes: PCB_COPPER_POUR_SHAPES,
|
|
250
|
+
simulationSourceKinds: SIMULATION_SOURCE_KINDS,
|
|
251
|
+
simulationWaveShapes: SIMULATION_WAVE_SHAPES,
|
|
252
|
+
simulationExperimentMethods: SIMULATION_EXPERIMENT_METHODS
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const SORTED_VARIANT_SET_NAMES = Object.keys(VARIANT_SETS).sort()
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @typedef {{ set: string, value: string }} CircuitJsonVariantDiff
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @typedef {{ elementTypes: string[], idFieldExceptions: string[], variantSets: Record<string, string[]> }} CircuitJsonSchemaSnapshot
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @typedef {{ matches: boolean, missingElementTypes: string[], unexpectedElementTypes: string[], missingIdFieldExceptions: string[], unexpectedIdFieldExceptions: string[], missingVariants: CircuitJsonVariantDiff[], unexpectedVariants: CircuitJsonVariantDiff[] }} CircuitJsonSchemaSnapshotComparison
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Validates serialized CircuitJSON element objects without external runtime
|
|
271
|
+
* dependencies.
|
|
272
|
+
*/
|
|
273
|
+
export class CircuitJsonElementValidator {
|
|
274
|
+
/**
|
|
275
|
+
* Returns validation errors for a candidate model.
|
|
276
|
+
* @param {unknown} value Candidate model.
|
|
277
|
+
* @returns {string[]}
|
|
278
|
+
*/
|
|
279
|
+
static validateModel(value) {
|
|
280
|
+
if (!Array.isArray(value)) {
|
|
281
|
+
return ['Expected a CircuitJSON element array.']
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return value.flatMap((element, index) =>
|
|
285
|
+
CircuitJsonElementValidator.validateElement(element, index)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Returns validation errors for one candidate element.
|
|
291
|
+
* @param {unknown} value Candidate element.
|
|
292
|
+
* @param {number} [index] Element index.
|
|
293
|
+
* @returns {string[]}
|
|
294
|
+
*/
|
|
295
|
+
static validateElement(value, index = -1) {
|
|
296
|
+
const location = index >= 0 ? ' at index ' + index : ''
|
|
297
|
+
if (!CircuitJsonElementValidator.#isObject(value)) {
|
|
298
|
+
return ['Expected a CircuitJSON element object' + location + '.']
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const type = String(value.type || '').trim()
|
|
302
|
+
if (!type) {
|
|
303
|
+
return ['CircuitJSON element type is required' + location + '.']
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!KNOWN_ELEMENT_TYPES.has(type)) {
|
|
307
|
+
return ['Unsupported CircuitJSON element type: ' + type + '.']
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const errors = []
|
|
311
|
+
CircuitJsonElementValidator.#validateId(value, type, errors)
|
|
312
|
+
CircuitJsonElementValidator.#validateCoreShape(value, type, errors)
|
|
313
|
+
return errors
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Returns all known serialized element type names.
|
|
318
|
+
* @returns {string[]}
|
|
319
|
+
*/
|
|
320
|
+
static knownElementTypes() {
|
|
321
|
+
return [...KNOWN_ELEMENT_TYPES]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Returns id convention exceptions from the current schema snapshot.
|
|
326
|
+
* @returns {string[]}
|
|
327
|
+
*/
|
|
328
|
+
static idFieldExceptions() {
|
|
329
|
+
return [...ID_FIELD_EXCEPTIONS]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Returns schema metadata suitable for drift snapshots.
|
|
334
|
+
* @returns {CircuitJsonSchemaSnapshot}
|
|
335
|
+
*/
|
|
336
|
+
static schemaSnapshot() {
|
|
337
|
+
return {
|
|
338
|
+
elementTypes: CircuitJsonElementValidator.knownElementTypes(),
|
|
339
|
+
idFieldExceptions: CircuitJsonElementValidator.idFieldExceptions(),
|
|
340
|
+
variantSets: CircuitJsonElementValidator.variantSets()
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Returns known variant discriminants from the active schema metadata.
|
|
346
|
+
* @returns {Record<string, string[]>}
|
|
347
|
+
*/
|
|
348
|
+
static variantSets() {
|
|
349
|
+
return Object.fromEntries(
|
|
350
|
+
SORTED_VARIANT_SET_NAMES.map((name) => [
|
|
351
|
+
name,
|
|
352
|
+
[...VARIANT_SETS[name]].sort()
|
|
353
|
+
])
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Compares current schema metadata against a saved snapshot.
|
|
359
|
+
* @param {{ elementTypes?: string[], idFieldExceptions?: string[], variantSets?: Record<string, string[]> }} snapshot Schema snapshot.
|
|
360
|
+
* @returns {CircuitJsonSchemaSnapshotComparison}
|
|
361
|
+
*/
|
|
362
|
+
static compareSchemaSnapshot(snapshot = {}) {
|
|
363
|
+
const elementComparison = CircuitJsonElementValidator.#compareSets(
|
|
364
|
+
new Set(snapshot.elementTypes || []),
|
|
365
|
+
KNOWN_ELEMENT_TYPES
|
|
366
|
+
)
|
|
367
|
+
const exceptionComparison = CircuitJsonElementValidator.#compareSets(
|
|
368
|
+
new Set(snapshot.idFieldExceptions || []),
|
|
369
|
+
ID_FIELD_EXCEPTIONS
|
|
370
|
+
)
|
|
371
|
+
const variantComparison =
|
|
372
|
+
CircuitJsonElementValidator.#compareVariantSets(
|
|
373
|
+
snapshot.variantSets || {}
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
matches:
|
|
378
|
+
elementComparison.missing.length === 0 &&
|
|
379
|
+
elementComparison.unexpected.length === 0 &&
|
|
380
|
+
exceptionComparison.missing.length === 0 &&
|
|
381
|
+
exceptionComparison.unexpected.length === 0 &&
|
|
382
|
+
variantComparison.missing.length === 0 &&
|
|
383
|
+
variantComparison.unexpected.length === 0,
|
|
384
|
+
missingElementTypes: elementComparison.missing,
|
|
385
|
+
unexpectedElementTypes: elementComparison.unexpected,
|
|
386
|
+
missingIdFieldExceptions: exceptionComparison.missing,
|
|
387
|
+
unexpectedIdFieldExceptions: exceptionComparison.unexpected,
|
|
388
|
+
missingVariants: variantComparison.missing,
|
|
389
|
+
unexpectedVariants: variantComparison.unexpected
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Validates the common id convention.
|
|
395
|
+
* @param {Record<string, unknown>} element Element.
|
|
396
|
+
* @param {string} type Element type.
|
|
397
|
+
* @param {string[]} errors Error sink.
|
|
398
|
+
* @returns {void}
|
|
399
|
+
*/
|
|
400
|
+
static #validateId(element, type, errors) {
|
|
401
|
+
if (ID_FIELD_EXCEPTIONS.has(type)) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const idField = type + '_id'
|
|
406
|
+
if (!CircuitJsonElementValidator.#isNonEmptyString(element[idField])) {
|
|
407
|
+
errors.push(type + ' ' + idField + ' is required.')
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Validates type-specific fields used by core consumers.
|
|
413
|
+
* @param {Record<string, unknown>} element Element.
|
|
414
|
+
* @param {string} type Element type.
|
|
415
|
+
* @param {string[]} errors Error sink.
|
|
416
|
+
* @returns {void}
|
|
417
|
+
*/
|
|
418
|
+
static #validateCoreShape(element, type, errors) {
|
|
419
|
+
if (type === 'source_component') {
|
|
420
|
+
CircuitJsonElementValidator.#validateSourceComponent(
|
|
421
|
+
element,
|
|
422
|
+
errors
|
|
423
|
+
)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (type === 'source_port') {
|
|
428
|
+
CircuitJsonElementValidator.#validateSourcePort(element, errors)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (type === 'schematic_component') {
|
|
433
|
+
CircuitJsonElementValidator.#validateSchematicComponent(
|
|
434
|
+
element,
|
|
435
|
+
errors
|
|
436
|
+
)
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (type === 'pcb_board') {
|
|
441
|
+
CircuitJsonElementValidator.#validatePcbBoard(element, errors)
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (type === 'pcb_component') {
|
|
446
|
+
CircuitJsonElementValidator.#validatePcbComponent(element, errors)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (type === 'pcb_smtpad') {
|
|
451
|
+
CircuitJsonElementValidator.#validatePcbSmtPad(element, errors)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (type === 'simulation_oscilloscope_trace') {
|
|
456
|
+
CircuitJsonElementValidator.#validateSimulationOscilloscopeTrace(
|
|
457
|
+
element,
|
|
458
|
+
errors
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Validates a source component.
|
|
465
|
+
* @param {Record<string, unknown>} element Element.
|
|
466
|
+
* @param {string[]} errors Error sink.
|
|
467
|
+
* @returns {void}
|
|
468
|
+
*/
|
|
469
|
+
static #validateSourceComponent(element, errors) {
|
|
470
|
+
CircuitJsonElementValidator.#requireString(
|
|
471
|
+
element,
|
|
472
|
+
'source_component',
|
|
473
|
+
'name',
|
|
474
|
+
errors
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if (
|
|
478
|
+
Object.hasOwn(element, 'supplier_part_numbers') &&
|
|
479
|
+
!CircuitJsonElementValidator.#isPlainObject(
|
|
480
|
+
element.supplier_part_numbers
|
|
481
|
+
)
|
|
482
|
+
) {
|
|
483
|
+
errors.push(
|
|
484
|
+
'source_component supplier_part_numbers must be an object.'
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (
|
|
489
|
+
Object.hasOwn(element, 'ftype') &&
|
|
490
|
+
!SOURCE_COMPONENT_FTYPES.has(String(element.ftype || ''))
|
|
491
|
+
) {
|
|
492
|
+
errors.push('source_component ftype is not supported.')
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Validates a source port.
|
|
498
|
+
* @param {Record<string, unknown>} element Element.
|
|
499
|
+
* @param {string[]} errors Error sink.
|
|
500
|
+
* @returns {void}
|
|
501
|
+
*/
|
|
502
|
+
static #validateSourcePort(element, errors) {
|
|
503
|
+
if (
|
|
504
|
+
Object.hasOwn(element, 'pin_number') &&
|
|
505
|
+
!Number.isFinite(element.pin_number)
|
|
506
|
+
) {
|
|
507
|
+
errors.push('source_port pin_number must be a number.')
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Validates a schematic component.
|
|
513
|
+
* @param {Record<string, unknown>} element Element.
|
|
514
|
+
* @param {string[]} errors Error sink.
|
|
515
|
+
* @returns {void}
|
|
516
|
+
*/
|
|
517
|
+
static #validateSchematicComponent(element, errors) {
|
|
518
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
519
|
+
element,
|
|
520
|
+
'schematic_component',
|
|
521
|
+
'center',
|
|
522
|
+
errors
|
|
523
|
+
)
|
|
524
|
+
CircuitJsonElementValidator.#requireSize(
|
|
525
|
+
element,
|
|
526
|
+
'schematic_component',
|
|
527
|
+
'size',
|
|
528
|
+
errors
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Validates a PCB board.
|
|
534
|
+
* @param {Record<string, unknown>} element Element.
|
|
535
|
+
* @param {string[]} errors Error sink.
|
|
536
|
+
* @returns {void}
|
|
537
|
+
*/
|
|
538
|
+
static #validatePcbBoard(element, errors) {
|
|
539
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
540
|
+
element,
|
|
541
|
+
'pcb_board',
|
|
542
|
+
'center',
|
|
543
|
+
errors
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Validates a PCB component.
|
|
549
|
+
* @param {Record<string, unknown>} element Element.
|
|
550
|
+
* @param {string[]} errors Error sink.
|
|
551
|
+
* @returns {void}
|
|
552
|
+
*/
|
|
553
|
+
static #validatePcbComponent(element, errors) {
|
|
554
|
+
CircuitJsonElementValidator.#requireString(
|
|
555
|
+
element,
|
|
556
|
+
'pcb_component',
|
|
557
|
+
'source_component_id',
|
|
558
|
+
errors
|
|
559
|
+
)
|
|
560
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
561
|
+
element,
|
|
562
|
+
'pcb_component',
|
|
563
|
+
'center',
|
|
564
|
+
errors
|
|
565
|
+
)
|
|
566
|
+
CircuitJsonElementValidator.#requireLayer(
|
|
567
|
+
element,
|
|
568
|
+
'pcb_component',
|
|
569
|
+
'layer',
|
|
570
|
+
errors
|
|
571
|
+
)
|
|
572
|
+
CircuitJsonElementValidator.#optionalAngle(
|
|
573
|
+
element,
|
|
574
|
+
'pcb_component',
|
|
575
|
+
'rotation',
|
|
576
|
+
errors
|
|
577
|
+
)
|
|
578
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
579
|
+
element,
|
|
580
|
+
'pcb_component',
|
|
581
|
+
'width',
|
|
582
|
+
errors
|
|
583
|
+
)
|
|
584
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
585
|
+
element,
|
|
586
|
+
'pcb_component',
|
|
587
|
+
'height',
|
|
588
|
+
errors
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Validates an SMT pad.
|
|
594
|
+
* @param {Record<string, unknown>} element Element.
|
|
595
|
+
* @param {string[]} errors Error sink.
|
|
596
|
+
* @returns {void}
|
|
597
|
+
*/
|
|
598
|
+
static #validatePcbSmtPad(element, errors) {
|
|
599
|
+
const shape = String(element.shape || '')
|
|
600
|
+
if (!SMT_PAD_SHAPES.has(shape)) {
|
|
601
|
+
errors.push(
|
|
602
|
+
'pcb_smtpad shape must be one of: ' +
|
|
603
|
+
[...SMT_PAD_SHAPES].join(', ') +
|
|
604
|
+
'.'
|
|
605
|
+
)
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
CircuitJsonElementValidator.#requireLayer(
|
|
610
|
+
element,
|
|
611
|
+
'pcb_smtpad',
|
|
612
|
+
'layer',
|
|
613
|
+
errors
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if (shape === 'polygon') {
|
|
617
|
+
if (!Array.isArray(element.points)) {
|
|
618
|
+
errors.push('pcb_smtpad points is required.')
|
|
619
|
+
}
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
CircuitJsonElementValidator.#requireLength(
|
|
624
|
+
element,
|
|
625
|
+
'pcb_smtpad',
|
|
626
|
+
'x',
|
|
627
|
+
errors
|
|
628
|
+
)
|
|
629
|
+
CircuitJsonElementValidator.#requireLength(
|
|
630
|
+
element,
|
|
631
|
+
'pcb_smtpad',
|
|
632
|
+
'y',
|
|
633
|
+
errors
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if (shape === 'circle') {
|
|
637
|
+
if (
|
|
638
|
+
!CircuitJsonElementValidator.#hasLength(element, 'radius') &&
|
|
639
|
+
!CircuitJsonElementValidator.#hasLength(element, 'diameter') &&
|
|
640
|
+
!(
|
|
641
|
+
CircuitJsonElementValidator.#hasLength(element, 'width') &&
|
|
642
|
+
CircuitJsonElementValidator.#hasLength(element, 'height')
|
|
643
|
+
)
|
|
644
|
+
) {
|
|
645
|
+
errors.push(
|
|
646
|
+
'pcb_smtpad radius, diameter, or width and height is required.'
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
CircuitJsonElementValidator.#requireLength(
|
|
653
|
+
element,
|
|
654
|
+
'pcb_smtpad',
|
|
655
|
+
'width',
|
|
656
|
+
errors
|
|
657
|
+
)
|
|
658
|
+
CircuitJsonElementValidator.#requireLength(
|
|
659
|
+
element,
|
|
660
|
+
'pcb_smtpad',
|
|
661
|
+
'height',
|
|
662
|
+
errors
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
if (shape.startsWith('rotated_')) {
|
|
666
|
+
CircuitJsonElementValidator.#requireAngle(
|
|
667
|
+
element,
|
|
668
|
+
'pcb_smtpad',
|
|
669
|
+
'ccw_rotation',
|
|
670
|
+
errors
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (shape.endsWith('pill')) {
|
|
675
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
676
|
+
element,
|
|
677
|
+
'pcb_smtpad',
|
|
678
|
+
'radius',
|
|
679
|
+
errors
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Validates oscilloscope trace reference and unit constraints.
|
|
686
|
+
* @param {Record<string, unknown>} element Element.
|
|
687
|
+
* @param {string[]} errors Error sink.
|
|
688
|
+
* @returns {void}
|
|
689
|
+
*/
|
|
690
|
+
static #validateSimulationOscilloscopeTrace(element, errors) {
|
|
691
|
+
const voltageReferences = [
|
|
692
|
+
element.simulation_transient_voltage_graph_id,
|
|
693
|
+
element.simulation_voltage_probe_id
|
|
694
|
+
].filter((value) =>
|
|
695
|
+
CircuitJsonElementValidator.#isNonEmptyString(value)
|
|
696
|
+
).length
|
|
697
|
+
const currentReferences = [
|
|
698
|
+
element.simulation_transient_current_graph_id,
|
|
699
|
+
element.simulation_current_probe_id
|
|
700
|
+
].filter((value) =>
|
|
701
|
+
CircuitJsonElementValidator.#isNonEmptyString(value)
|
|
702
|
+
).length
|
|
703
|
+
|
|
704
|
+
if (voltageReferences + currentReferences !== 1) {
|
|
705
|
+
errors.push(
|
|
706
|
+
'simulation_oscilloscope_trace must reference exactly one graph or probe.'
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
if (voltageReferences > 0 && Object.hasOwn(element, 'amps_per_div')) {
|
|
710
|
+
errors.push(
|
|
711
|
+
'voltage oscilloscope traces must use volts_per_div, not amps_per_div.'
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
if (currentReferences > 0 && Object.hasOwn(element, 'volts_per_div')) {
|
|
715
|
+
errors.push(
|
|
716
|
+
'current oscilloscope traces must use amps_per_div, not volts_per_div.'
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
CircuitJsonElementValidator.#optionalPositiveNumber(
|
|
720
|
+
element,
|
|
721
|
+
'simulation_oscilloscope_trace',
|
|
722
|
+
'volts_per_div',
|
|
723
|
+
errors
|
|
724
|
+
)
|
|
725
|
+
CircuitJsonElementValidator.#optionalPositiveNumber(
|
|
726
|
+
element,
|
|
727
|
+
'simulation_oscilloscope_trace',
|
|
728
|
+
'amps_per_div',
|
|
729
|
+
errors
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Requires a non-empty string field.
|
|
735
|
+
* @param {Record<string, unknown>} element Element.
|
|
736
|
+
* @param {string} type Element type.
|
|
737
|
+
* @param {string} field Field name.
|
|
738
|
+
* @param {string[]} errors Error sink.
|
|
739
|
+
* @returns {void}
|
|
740
|
+
*/
|
|
741
|
+
static #requireString(element, type, field, errors) {
|
|
742
|
+
if (!CircuitJsonElementValidator.#isNonEmptyString(element[field])) {
|
|
743
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Requires a finite number field.
|
|
749
|
+
* @param {Record<string, unknown>} element Element.
|
|
750
|
+
* @param {string} type Element type.
|
|
751
|
+
* @param {string} field Field name.
|
|
752
|
+
* @param {string[]} errors Error sink.
|
|
753
|
+
* @returns {void}
|
|
754
|
+
*/
|
|
755
|
+
static #requireNumber(element, type, field, errors) {
|
|
756
|
+
if (!Number.isFinite(element[field])) {
|
|
757
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Requires a finite length field.
|
|
763
|
+
* @param {Record<string, unknown>} element Element.
|
|
764
|
+
* @param {string} type Element type.
|
|
765
|
+
* @param {string} field Field name.
|
|
766
|
+
* @param {string[]} errors Error sink.
|
|
767
|
+
* @returns {void}
|
|
768
|
+
*/
|
|
769
|
+
static #requireLength(element, type, field, errors) {
|
|
770
|
+
if (CircuitJsonUnits.optionalLength(element[field]) === null) {
|
|
771
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Returns true when a field is a finite length.
|
|
777
|
+
* @param {Record<string, unknown>} element Element.
|
|
778
|
+
* @param {string} field Field name.
|
|
779
|
+
* @returns {boolean}
|
|
780
|
+
*/
|
|
781
|
+
static #hasLength(element, field) {
|
|
782
|
+
return CircuitJsonUnits.optionalLength(element[field]) !== null
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Requires a finite angle field.
|
|
787
|
+
* @param {Record<string, unknown>} element Element.
|
|
788
|
+
* @param {string} type Element type.
|
|
789
|
+
* @param {string} field Field name.
|
|
790
|
+
* @param {string[]} errors Error sink.
|
|
791
|
+
* @returns {void}
|
|
792
|
+
*/
|
|
793
|
+
static #requireAngle(element, type, field, errors) {
|
|
794
|
+
if (CircuitJsonUnits.optionalAngle(element[field]) === null) {
|
|
795
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Validates an optional finite length field.
|
|
801
|
+
* @param {Record<string, unknown>} element Element.
|
|
802
|
+
* @param {string} type Element type.
|
|
803
|
+
* @param {string} field Field name.
|
|
804
|
+
* @param {string[]} errors Error sink.
|
|
805
|
+
* @returns {void}
|
|
806
|
+
*/
|
|
807
|
+
static #optionalLength(element, type, field, errors) {
|
|
808
|
+
if (
|
|
809
|
+
Object.hasOwn(element, field) &&
|
|
810
|
+
CircuitJsonUnits.optionalLength(element[field]) === null
|
|
811
|
+
) {
|
|
812
|
+
errors.push(type + ' ' + field + ' must be a finite length.')
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Validates an optional finite angle field.
|
|
818
|
+
* @param {Record<string, unknown>} element Element.
|
|
819
|
+
* @param {string} type Element type.
|
|
820
|
+
* @param {string} field Field name.
|
|
821
|
+
* @param {string[]} errors Error sink.
|
|
822
|
+
* @returns {void}
|
|
823
|
+
*/
|
|
824
|
+
static #optionalAngle(element, type, field, errors) {
|
|
825
|
+
if (
|
|
826
|
+
Object.hasOwn(element, field) &&
|
|
827
|
+
CircuitJsonUnits.optionalAngle(element[field]) === null
|
|
828
|
+
) {
|
|
829
|
+
errors.push(type + ' ' + field + ' must be a finite angle.')
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Validates an optional positive finite number field.
|
|
835
|
+
* @param {Record<string, unknown>} element Element.
|
|
836
|
+
* @param {string} type Element type.
|
|
837
|
+
* @param {string} field Field name.
|
|
838
|
+
* @param {string[]} errors Error sink.
|
|
839
|
+
* @returns {void}
|
|
840
|
+
*/
|
|
841
|
+
static #optionalPositiveNumber(element, type, field, errors) {
|
|
842
|
+
if (!Object.hasOwn(element, field)) {
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
const value = Number(element[field])
|
|
846
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
847
|
+
errors.push(type + ' ' + field + ' must be a positive number.')
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Requires a finite point object field.
|
|
853
|
+
* @param {Record<string, unknown>} element Element.
|
|
854
|
+
* @param {string} type Element type.
|
|
855
|
+
* @param {string} field Field name.
|
|
856
|
+
* @param {string[]} errors Error sink.
|
|
857
|
+
* @returns {void}
|
|
858
|
+
*/
|
|
859
|
+
static #requirePoint(element, type, field, errors) {
|
|
860
|
+
const point = element[field]
|
|
861
|
+
if (!CircuitJsonElementValidator.#isObject(point)) {
|
|
862
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (CircuitJsonUnits.optionalPoint(point) === null) {
|
|
867
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Requires a finite size object field.
|
|
873
|
+
* @param {Record<string, unknown>} element Element.
|
|
874
|
+
* @param {string} type Element type.
|
|
875
|
+
* @param {string} field Field name.
|
|
876
|
+
* @param {string[]} errors Error sink.
|
|
877
|
+
* @returns {void}
|
|
878
|
+
*/
|
|
879
|
+
static #requireSize(element, type, field, errors) {
|
|
880
|
+
const size = element[field]
|
|
881
|
+
if (!CircuitJsonElementValidator.#isObject(size)) {
|
|
882
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (CircuitJsonUnits.optionalSize(size) === null) {
|
|
887
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Requires a known layer field.
|
|
893
|
+
* @param {Record<string, unknown>} element Element.
|
|
894
|
+
* @param {string} type Element type.
|
|
895
|
+
* @param {string} field Field name.
|
|
896
|
+
* @param {string[]} errors Error sink.
|
|
897
|
+
* @returns {void}
|
|
898
|
+
*/
|
|
899
|
+
static #requireLayer(element, type, field, errors) {
|
|
900
|
+
const layer =
|
|
901
|
+
typeof element[field] === 'object' && element[field] !== null
|
|
902
|
+
? element[field].name
|
|
903
|
+
: element[field]
|
|
904
|
+
if (!LAYERS.has(String(layer || ''))) {
|
|
905
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Returns true for non-null objects.
|
|
911
|
+
* @param {unknown} value Candidate.
|
|
912
|
+
* @returns {boolean}
|
|
913
|
+
*/
|
|
914
|
+
static #isObject(value) {
|
|
915
|
+
return Boolean(value) && typeof value === 'object'
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Returns true for plain object values.
|
|
920
|
+
* @param {unknown} value Candidate.
|
|
921
|
+
* @returns {boolean}
|
|
922
|
+
*/
|
|
923
|
+
static #isPlainObject(value) {
|
|
924
|
+
return (
|
|
925
|
+
CircuitJsonElementValidator.#isObject(value) &&
|
|
926
|
+
!Array.isArray(value)
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Returns true for non-empty strings.
|
|
932
|
+
* @param {unknown} value Candidate.
|
|
933
|
+
* @returns {boolean}
|
|
934
|
+
*/
|
|
935
|
+
static #isNonEmptyString(value) {
|
|
936
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Compares expected and actual string sets.
|
|
941
|
+
* @param {Set<string>} expected Expected values.
|
|
942
|
+
* @param {Set<string>} actual Actual values.
|
|
943
|
+
* @returns {{ missing: string[], unexpected: string[] }}
|
|
944
|
+
*/
|
|
945
|
+
static #compareSets(expected, actual) {
|
|
946
|
+
return {
|
|
947
|
+
missing: [...expected]
|
|
948
|
+
.filter((value) => !actual.has(value))
|
|
949
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
950
|
+
unexpected: [...actual]
|
|
951
|
+
.filter((value) => !expected.has(value))
|
|
952
|
+
.sort((left, right) => left.localeCompare(right))
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Compares expected and actual variant metadata sets.
|
|
958
|
+
* @param {Record<string, string[]>} snapshotSets Snapshot variant sets.
|
|
959
|
+
* @returns {{ missing: CircuitJsonVariantDiff[], unexpected: CircuitJsonVariantDiff[] }}
|
|
960
|
+
*/
|
|
961
|
+
static #compareVariantSets(snapshotSets) {
|
|
962
|
+
const missing = []
|
|
963
|
+
const unexpected = []
|
|
964
|
+
const setNames = new Set([
|
|
965
|
+
...SORTED_VARIANT_SET_NAMES,
|
|
966
|
+
...Object.keys(snapshotSets || {})
|
|
967
|
+
])
|
|
968
|
+
|
|
969
|
+
for (const setName of [...setNames].sort()) {
|
|
970
|
+
const comparison = CircuitJsonElementValidator.#compareSets(
|
|
971
|
+
new Set((snapshotSets?.[setName] || []).map(String)),
|
|
972
|
+
new Set(VARIANT_SETS[setName] || [])
|
|
973
|
+
)
|
|
974
|
+
missing.push(
|
|
975
|
+
...comparison.missing.map((value) => ({
|
|
976
|
+
set: setName,
|
|
977
|
+
value
|
|
978
|
+
}))
|
|
979
|
+
)
|
|
980
|
+
unexpected.push(
|
|
981
|
+
...comparison.unexpected.map((value) => ({
|
|
982
|
+
set: setName,
|
|
983
|
+
value
|
|
984
|
+
}))
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return { missing, unexpected }
|
|
989
|
+
}
|
|
990
|
+
}
|