opencdd 0.1.1
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.
- checksums.yaml +7 -0
- data/CLAUDE.md +486 -0
- data/README.md +304 -0
- data/bin/lint-no-raw-mdc +41 -0
- data/lib/cdd.rb +11 -0
- data/lib/opencdd/alias_table.rb +52 -0
- data/lib/opencdd/cddal/ast.rb +151 -0
- data/lib/opencdd/cddal/builder.rb +374 -0
- data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
- data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
- data/lib/opencdd/cddal/fetcher.rb +18 -0
- data/lib/opencdd/cddal/generated_parser.rb +805 -0
- data/lib/opencdd/cddal/lexer.rb +193 -0
- data/lib/opencdd/cddal/parser.rb +19 -0
- data/lib/opencdd/cddal/resolver.rb +100 -0
- data/lib/opencdd/cddal/serializer.rb +210 -0
- data/lib/opencdd/cddal/value_serializer.rb +60 -0
- data/lib/opencdd/cddal.rb +63 -0
- data/lib/opencdd/class_tree.rb +80 -0
- data/lib/opencdd/class_type.rb +33 -0
- data/lib/opencdd/codegen/ts.rb +185 -0
- data/lib/opencdd/codegen.rb +7 -0
- data/lib/opencdd/composition_tree.rb +119 -0
- data/lib/opencdd/condition.rb +120 -0
- data/lib/opencdd/data_type.rb +143 -0
- data/lib/opencdd/database.rb +719 -0
- data/lib/opencdd/effective_properties.rb +119 -0
- data/lib/opencdd/entity/field_reader.rb +141 -0
- data/lib/opencdd/entity/field_registry.rb +99 -0
- data/lib/opencdd/entity/version_history.rb +62 -0
- data/lib/opencdd/entity.rb +255 -0
- data/lib/opencdd/exporters/json.rb +235 -0
- data/lib/opencdd/exporters/mermaid.rb +62 -0
- data/lib/opencdd/exporters/yaml.rb +16 -0
- data/lib/opencdd/exporters.rb +9 -0
- data/lib/opencdd/guid.rb +27 -0
- data/lib/opencdd/instance_rule.rb +70 -0
- data/lib/opencdd/irdi.rb +128 -0
- data/lib/opencdd/klass.rb +230 -0
- data/lib/opencdd/languages.rb +70 -0
- data/lib/opencdd/meta_class.rb +274 -0
- data/lib/opencdd/model/entity_store.rb +85 -0
- data/lib/opencdd/model/yaml_database.rb +49 -0
- data/lib/opencdd/model/yaml_entity.rb +196 -0
- data/lib/opencdd/model.rb +13 -0
- data/lib/opencdd/parcel/csv_reader.rb +35 -0
- data/lib/opencdd/parcel/csv_writer.rb +114 -0
- data/lib/opencdd/parcel/entity_manifest.rb +119 -0
- data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
- data/lib/opencdd/parcel/layout_detector.rb +115 -0
- data/lib/opencdd/parcel/metadata.rb +112 -0
- data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
- data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
- data/lib/opencdd/parcel/selector.rb +115 -0
- data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
- data/lib/opencdd/parcel/sheet.rb +287 -0
- data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
- data/lib/opencdd/parcel/sheet_schema.rb +253 -0
- data/lib/opencdd/parcel/workbook.rb +172 -0
- data/lib/opencdd/parcel/workbook_reader.rb +200 -0
- data/lib/opencdd/parcel/writer.rb +185 -0
- data/lib/opencdd/parcel.rb +175 -0
- data/lib/opencdd/parse_helpers.rb +73 -0
- data/lib/opencdd/property.rb +120 -0
- data/lib/opencdd/property_data_element_type.rb +44 -0
- data/lib/opencdd/property_ids.rb +202 -0
- data/lib/opencdd/reader.rb +103 -0
- data/lib/opencdd/relation.rb +88 -0
- data/lib/opencdd/relation_tree.rb +74 -0
- data/lib/opencdd/relation_type.rb +58 -0
- data/lib/opencdd/structured_values.rb +183 -0
- data/lib/opencdd/unit.rb +16 -0
- data/lib/opencdd/validator/class_reference_rule.rb +77 -0
- data/lib/opencdd/validator/condition_rule.rb +27 -0
- data/lib/opencdd/validator/data_type_rule.rb +26 -0
- data/lib/opencdd/validator/enum_rule.rb +27 -0
- data/lib/opencdd/validator/format_rule.rb +54 -0
- data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
- data/lib/opencdd/validator/irdi_rule.rb +57 -0
- data/lib/opencdd/validator/mandatory_rule.rb +25 -0
- data/lib/opencdd/validator/pattern_rule.rb +26 -0
- data/lib/opencdd/validator/reference_rule.rb +36 -0
- data/lib/opencdd/validator/rule.rb +65 -0
- data/lib/opencdd/validator/runner.rb +101 -0
- data/lib/opencdd/validator/set_rule.rb +41 -0
- data/lib/opencdd/validator/synonym_rule.rb +30 -0
- data/lib/opencdd/validator/type_rule.rb +102 -0
- data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
- data/lib/opencdd/validator.rb +68 -0
- data/lib/opencdd/value_format.rb +71 -0
- data/lib/opencdd/value_list.rb +42 -0
- data/lib/opencdd/value_term.rb +24 -0
- data/lib/opencdd/version.rb +5 -0
- data/lib/opencdd/view_control.rb +10 -0
- data/lib/opencdd/visitor.rb +93 -0
- data/lib/opencdd.rb +57 -0
- metadata +325 -0
data/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# opencdd — Ruby model for the IEC Common Data Dictionary
|
|
2
|
+
|
|
3
|
+
[](https://github.com/opencdd/opencdd-ruby/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/opencdd)
|
|
5
|
+
|
|
6
|
+
**opencdd** is a pure-Ruby library for reading, writing, validating, and
|
|
7
|
+
navigating the IEC Common Data Dictionary (CDD) — the ISO/IEC 61360 / 62656-1
|
|
8
|
+
ontology used by IEC CDD at `cdd.iec.ch`, by ParcelMaker, and by downstream
|
|
9
|
+
engineering and BOM tooling.
|
|
10
|
+
|
|
11
|
+
It is the Ruby core of the OpenCDD ecosystem:
|
|
12
|
+
|
|
13
|
+
- [`opencdd/cdd-models-ts`](https://github.com/opencdd/cdd-models-ts) — TypeScript port (model layer)
|
|
14
|
+
- [`opencdd/editor`](https://github.com/opencdd/editor) — Browser-based CDD editor
|
|
15
|
+
- [`opencdd/opencdd.github.io`](https://github.com/opencdd/opencdd.github.io) — Static-site dictionary browser
|
|
16
|
+
- [`opencdd/cddal-spec`](https://github.com/opencdd/cddal-spec) — The CDDAL format specification
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why CDD? Why this gem?
|
|
21
|
+
|
|
22
|
+
CDD is a **four-layer ontology** standardised by IEC/TC 184/SC 4/JWG 24 and
|
|
23
|
+
IEC/SC 3D. It models engineering dictionaries (components, materials,
|
|
24
|
+
quantities, units) with one distinctive capability that separates it from
|
|
25
|
+
UML, RDF/OWL, and typical object models:
|
|
26
|
+
|
|
27
|
+
> **A class declared `CATEGORICAL_CLASS` at the model layer has subclasses
|
|
28
|
+
> that are themselves classes, but are also treated as instances of the
|
|
29
|
+
> categorical class.** This is called *powertype modelling*.
|
|
30
|
+
|
|
31
|
+
In UML/RDF, an "instance" is a terminal object. In CDD, an "instance" of a
|
|
32
|
+
categorical class is *another class* that can be further specialised. This
|
|
33
|
+
two-level capability lets CDD express configurable product hierarchies
|
|
34
|
+
("this product line offers engine options {SingleDiesel, TwinDiesel,
|
|
35
|
+
ElectricHybrid}") without leaving the type system.
|
|
36
|
+
|
|
37
|
+
`opencdd` preserves this capability as a first-class concept — see
|
|
38
|
+
`Klass#powertype?`, `Database#instances_of`, and the
|
|
39
|
+
[ontology guide](docs/ontology.md).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Add to your `Gemfile`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
gem "opencdd"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install from source:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/opencdd/opencdd-ruby.git
|
|
55
|
+
cd opencdd-ruby
|
|
56
|
+
bundle install
|
|
57
|
+
bundle exec rake build
|
|
58
|
+
gem install pkg/opencdd-*.gem
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires Ruby ≥ 3.1.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "opencdd"
|
|
69
|
+
|
|
70
|
+
# Load a Parcel-format dictionary (sharded per-class layout)
|
|
71
|
+
db = Opencdd::Database.load("downloads/iec63213")
|
|
72
|
+
|
|
73
|
+
# Find a class and walk its hierarchy
|
|
74
|
+
vehicle = db.find_by_code("AAA001")
|
|
75
|
+
puts vehicle.preferred_name # => "Vehicle"
|
|
76
|
+
puts vehicle.children.map(&:code) # => ["AAA010", "AAA020", "AAA030", ...]
|
|
77
|
+
puts vehicle.effective_properties.map(&:code) # inherited + declared properties
|
|
78
|
+
|
|
79
|
+
# Powertype API: what configuration options does EngineType offer?
|
|
80
|
+
engine_type = db.find_by_code("AAA200")
|
|
81
|
+
engine_type.powertype? # => true
|
|
82
|
+
db.instances_of(engine_type).map(&:preferred_name)
|
|
83
|
+
# => ["Single Diesel Engine", "Twin Diesel Engine", "Electric Hybrid Engine"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Parse CDDAL (the plain-text canonical format)
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
db = Opencdd::Cddal.parse_file("path/to/dictionary.cddal")
|
|
90
|
+
# => Opencdd::Database populated with classes, properties, units, ...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Read a Parcel `.xlsx` workbook
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
db = Opencdd::Parcel::WorkbookReader
|
|
97
|
+
.new("export_CDD_IEC62683 in ParcelMaker format.xlsx")
|
|
98
|
+
.load_into(Opencdd::Database.new)
|
|
99
|
+
db.classes.size # => 250+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Round-trip CDDAL → Parcel → CDDAL
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
db = Opencdd::Cddal.parse_file("oceanrunner.cddal")
|
|
106
|
+
xlsx = Tempfile.new(["out", ".xlsx"]).path
|
|
107
|
+
Opencdd::Parcel::Writer.new(db).write(xlsx, parcel_id: "OCEANRUNNER")
|
|
108
|
+
|
|
109
|
+
db2 = Opencdd::Database.load(xlsx)
|
|
110
|
+
db.semantically_equal?(db2) # => true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Validate
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
errors = Opencdd::Validator.run(db)
|
|
117
|
+
errors.first.message # => "R08: reference \"UNIVERSE\" does not resolve..."
|
|
118
|
+
errors.first.rule # => "R08"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## What you can do with this gem
|
|
124
|
+
|
|
125
|
+
| Task | Class / method |
|
|
126
|
+
|------|---------------|
|
|
127
|
+
| Read Parcel `.xlsx` (ParcelMaker 5.2.1 layout) | `Opencdd::Parcel::WorkbookReader` |
|
|
128
|
+
| Read legacy 6-file `.xls` layout | `Opencdd::Parcel::FlatDirReader` |
|
|
129
|
+
| Read per-class sharded `.xls` (harvester output) | `Opencdd::Parcel::ShardedDirReader` |
|
|
130
|
+
| Write Parcel `.xlsx` | `Opencdd::Parcel::Writer` |
|
|
131
|
+
| Parse / serialize CDDAL (plain-text format) | `Opencdd::Cddal.parse`, `Opencdd::Cddal.serialize` |
|
|
132
|
+
| Split / aggregate dictionaries | `Opencdd::Parcel.split`, `Opencdd::Parcel.aggregate` |
|
|
133
|
+
| Validate against IEC 61360 rules | `Opencdd::Validator.run` (R01–R16) |
|
|
134
|
+
| Export to JSON / YAML / Mermaid | `Opencdd::Exporters::{Json,Yaml,Mermaid}` |
|
|
135
|
+
| Powertype queries | `Klass#powertype?`, `Database#instances_of`, `Database#valid_class_reference?` |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## The four-layer CDD ontology
|
|
140
|
+
|
|
141
|
+
CDD is layered per IEC 61360 §5:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
M2 Meta-model The meta-classes: Class, Property, Unit, ValueList,
|
|
145
|
+
ValueTerm, Relation, ViewControl.
|
|
146
|
+
Modeled as Opencdd::MetaClass instances.
|
|
147
|
+
Fixed set per the standard.
|
|
148
|
+
|
|
149
|
+
M1 Model The dictionary content: AAA001 "Vehicle",
|
|
150
|
+
AAAP001 "vehicle length", etc. Instances of M2
|
|
151
|
+
meta-classes. Modeled as Opencdd::Entity
|
|
152
|
+
subclasses (Klass, Property, Unit, ...).
|
|
153
|
+
|
|
154
|
+
M0 Data Real-world individuals: serial-numbered units,
|
|
155
|
+
specific product configurations. Instances of
|
|
156
|
+
M1 classes.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**The powertype distinction**: at M1, a class declared
|
|
160
|
+
`class_type=CATEGORICAL_CLASS` has subclasses that are *also* its
|
|
161
|
+
instances in the categorical sense. Used for:
|
|
162
|
+
|
|
163
|
+
- `CLASS_REFERENCE(EngineType)` — a property value constrained to one of
|
|
164
|
+
EngineType's categorical instances
|
|
165
|
+
- `sub_class_selection: { TwinDiesel, Premium, CarbonBlack }` — a
|
|
166
|
+
configured subclass nailing down categorical options
|
|
167
|
+
|
|
168
|
+
See [`docs/ontology.md`](docs/ontology.md) for the full walkthrough.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Architecture
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
lib/opencdd/
|
|
176
|
+
├── opencdd.rb # canonical entry (autoload tree)
|
|
177
|
+
├── cdd.rb # back-compat shim: require "opencdd"
|
|
178
|
+
├── version.rb
|
|
179
|
+
│
|
|
180
|
+
├── entity.rb # base entity + field DSL
|
|
181
|
+
├── klass.rb # class entity + powertype API
|
|
182
|
+
├── property.rb, unit.rb, value_list.rb, value_term.rb,
|
|
183
|
+
├── relation.rb, view_control.rb
|
|
184
|
+
│
|
|
185
|
+
├── meta_class.rb # M2 meta-classes (SSOT for type/sheet mapping)
|
|
186
|
+
├── property_ids.rb # SSOT for MDC_P### identifiers
|
|
187
|
+
├── alias_table.rb # alias → property ID resolution
|
|
188
|
+
├── irdi.rb # IRDI parsing (3 forms)
|
|
189
|
+
├── class_type.rb, data_type.rb, value_format.rb,
|
|
190
|
+
├── condition.rb, property_data_element_type.rb,
|
|
191
|
+
├── relation_type.rb, languages.rb
|
|
192
|
+
│
|
|
193
|
+
├── database.rb # in-memory store; finalize! / merge / find
|
|
194
|
+
├── effective_properties.rb # cycle-safe walker
|
|
195
|
+
├── class_tree.rb, composition_tree.rb, relation_tree.rb
|
|
196
|
+
│
|
|
197
|
+
├── parcel/ # IEC 62656-1 Parcel format
|
|
198
|
+
│ ├── workbook_reader.rb, flat_dir_reader.rb, sharded_dir_reader.rb
|
|
199
|
+
│ ├── writer.rb, csv_writer.rb, csv_reader.rb
|
|
200
|
+
│ ├── sheet.rb, sheet_schema.rb, sheet_emitter.rb
|
|
201
|
+
│ ├── metadata.rb, workbook.rb, selector.rb, referenced_irdis.rb
|
|
202
|
+
│
|
|
203
|
+
├── cddal/ # plain-text canonical format
|
|
204
|
+
│ ├── lexer.rb, cddal.y, generated_parser.rb, parser.rb
|
|
205
|
+
│ ├── ast.rb, builder.rb, serializer.rb
|
|
206
|
+
│ ├── resolver.rb # module/import path/URL resolution
|
|
207
|
+
│ └── fetcher/ # pluggable URL fetcher (NetHttp, InMemory)
|
|
208
|
+
│
|
|
209
|
+
├── validator/ # R01–R16 rules + Runner
|
|
210
|
+
│ ├── irdi_rule.rb, uniqueness_rule.rb, mandatory_rule.rb,
|
|
211
|
+
│ ├── type_rule.rb, enum_rule.rb, format_rule.rb, pattern_rule.rb,
|
|
212
|
+
│ ├── reference_rule.rb, set_rule.rb, synonym_rule.rb,
|
|
213
|
+
│ ├── condition_rule.rb, data_type_rule.rb, hierarchy_rule.rb,
|
|
214
|
+
│ └── class_reference_rule.rb # R16 — uses powertype API
|
|
215
|
+
│
|
|
216
|
+
├── exporters/ # json.rb, yaml.rb, mermaid.rb
|
|
217
|
+
├── codegen/ # ts.rb — generates PropertyIds.generated.ts
|
|
218
|
+
└── entity/
|
|
219
|
+
├── field_registry.rb # DSL: field :name, "MDC_P###"
|
|
220
|
+
├── field_reader.rb # instance_exec dispatch (no send-to-private)
|
|
221
|
+
└── version_history.rb
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Principles followed
|
|
225
|
+
|
|
226
|
+
- **OCP**: adding an entity type / validator rule / exporter = adding a
|
|
227
|
+
class, not editing a switch.
|
|
228
|
+
- **DRY**: one source of truth per concept. `MetaClasses` owns the
|
|
229
|
+
type→class→sheet-type mapping; `PropertyIds::REGISTRY` owns every
|
|
230
|
+
property ID; no raw `"MDC_P###"` literals outside the registry files
|
|
231
|
+
(enforced by `bin/lint-no-raw-mdc`).
|
|
232
|
+
- **MECE**: each concern lives in exactly one place. Parcel format
|
|
233
|
+
readers don't know about CDDAL; the validator doesn't know about
|
|
234
|
+
formats; the model doesn't know about Parcel.
|
|
235
|
+
- **Encapsulation**: no `send` to private methods (enforced by
|
|
236
|
+
`spec/code_quality_spec.rb`); entity state mutated only through named
|
|
237
|
+
mutators.
|
|
238
|
+
- **`autoload`, not `require_relative`**: load paths declared in the
|
|
239
|
+
immediate parent namespace file (enforced by `spec/code_quality_spec.rb`).
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Commands
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
bundle install # install dependencies
|
|
247
|
+
bundle exec rake spec # full spec suite (~700 examples)
|
|
248
|
+
bundle exec rake spec:data # data-fixture specs only (faster)
|
|
249
|
+
bundle exec rake build # build pkg/opencdd-*.gem
|
|
250
|
+
bundle exec rake cddal:regen # regenerate racc parser from cddal.y
|
|
251
|
+
bundle exec rake cddal:check_regen # fail CI if parser is stale
|
|
252
|
+
bundle exec rake lint:registry # forbid raw MDC_P### outside registry
|
|
253
|
+
bundle exec rake generate_ts # regenerate cdd-models-ts files
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Documentation
|
|
259
|
+
|
|
260
|
+
**Live docs site: <https://opencdd.github.io/opencdd-ruby/>**
|
|
261
|
+
|
|
262
|
+
The site is built with [Astro Starlight](https://starlight.astro.build/)
|
|
263
|
+
from the Markdown sources in [`docs/src/content/docs/`](docs/src/content/docs/).
|
|
264
|
+
It auto-deploys via GitHub Actions on every push to `main` that touches
|
|
265
|
+
`docs/`. Source files are also browseable on GitHub.
|
|
266
|
+
|
|
267
|
+
- **[Getting started](docs/src/content/docs/getting-started.md)** — 15-minute tutorial
|
|
268
|
+
- **[The four-layer ontology](docs/src/content/docs/ontology.md)** — CDD's powertype distinction
|
|
269
|
+
- **[CDDAL syntax](docs/src/content/docs/cddal-syntax.md)** — authoring plain-text CDD
|
|
270
|
+
- **[Parcel format](docs/src/content/docs/parcel-format.md)** — reading and writing Parcel `.xlsx`
|
|
271
|
+
- **[Architecture](docs/src/content/docs/architecture.md)** — gem internals and extension points
|
|
272
|
+
- **[Feature audit](docs/src/content/docs/reference/features.md)** — every feature, its status, and where to find it
|
|
273
|
+
- **[API reference](docs/src/content/docs/reference/api.md)** — key classes and methods
|
|
274
|
+
- **[Validator rules](docs/src/content/docs/reference/validator-rules.md)** — R01–R16 catalogue
|
|
275
|
+
- **[TODO.impl/](TODO.impl/)** — engineering plans, audit findings, open questions
|
|
276
|
+
|
|
277
|
+
## Examples
|
|
278
|
+
|
|
279
|
+
- [`reference-docs/examples/oceanrunner.cddal`](reference-docs/examples/oceanrunner.cddal) — the canonical powertype demo (40 entities, 20 classes)
|
|
280
|
+
- [`reference-docs/202003-kagoshima-iec-def-sample.cddal`](reference-docs/202003-kagoshima-iec-def-sample.cddal) — minimal bare-`instance` form
|
|
281
|
+
|
|
282
|
+
## Standards background
|
|
283
|
+
|
|
284
|
+
- **IEC 61360-1** — Terms and definitions
|
|
285
|
+
- **IEC 61360-2** — Data specification
|
|
286
|
+
- **IEC 61360-6** — Common Data Dictionary ontology
|
|
287
|
+
- **IEC 62656-1** — Parcel (Excel) format for CDD content
|
|
288
|
+
- **ISO/IEC 11179-6** — IRDI (registration data identifier)
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
PRs welcome. Please:
|
|
297
|
+
|
|
298
|
+
1. Open an issue first for non-trivial changes.
|
|
299
|
+
2. Follow the principles in [`CLAUDE.md`](CLAUDE.md) and
|
|
300
|
+
[`docs/architecture.md`](docs/architecture.md).
|
|
301
|
+
3. Add specs for any new behavior — the `code_quality_spec.rb` enforces
|
|
302
|
+
the architectural rules (no `send` to private, no `require_relative`,
|
|
303
|
+
no `instance_variable_set`, etc.).
|
|
304
|
+
4. Do not add AI-attribution trailers to commit messages.
|
data/bin/lint-no-raw-mdc
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# lint-no-raw-mdc — fail if any raw MDC_P### / MDC_C### / EXT_P### /
|
|
2
|
+
# EXT_C### / CIM_P### string literal appears in lib/opencdd/ outside
|
|
3
|
+
# the designated SSOT files.
|
|
4
|
+
#
|
|
5
|
+
# Allowed homes for raw MDC/EXT/CIM literals:
|
|
6
|
+
#
|
|
7
|
+
# - lib/opencdd/property_ids.rb — the ontology registry (SSOT)
|
|
8
|
+
# - lib/opencdd/meta_class.rb — meta-class definitions
|
|
9
|
+
# - lib/opencdd/alias_table.rb — alias defaults
|
|
10
|
+
# - lib/opencdd/parcel/sheet_schema.rb — Parcel-sheet variant→canonical
|
|
11
|
+
# column mapping (format-specific SSOT)
|
|
12
|
+
# - lib/opencdd/cddal/generated_parser.rb — racc-generated (machine output)
|
|
13
|
+
#
|
|
14
|
+
# Comment lines (lines whose first non-whitespace character is '#')
|
|
15
|
+
# are also exempt.
|
|
16
|
+
#
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
excludes=(
|
|
20
|
+
':!lib/opencdd/property_ids.rb'
|
|
21
|
+
':!lib/opencdd/meta_class.rb'
|
|
22
|
+
':!lib/opencdd/alias_table.rb'
|
|
23
|
+
':!lib/opencdd/parcel/sheet_schema.rb'
|
|
24
|
+
':!lib/opencdd/cddal/generated_parser.rb'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Collect hits, filtering out Ruby comment-only lines.
|
|
28
|
+
hits=$(git grep -nE '"(MDC_[CP][0-9]+(_[0-9]+)?|EXT_[CP][0-9]+|CIM_P[0-9]+)"' \
|
|
29
|
+
-- 'lib/opencdd/**/*.rb' "${excludes[@]}" 2>/dev/null \
|
|
30
|
+
| grep -vE '^[^:]+:[0-9]+:\s*#' \
|
|
31
|
+
|| true)
|
|
32
|
+
|
|
33
|
+
if [ -n "$hits" ]; then
|
|
34
|
+
echo "ERROR: raw MDC/EXT/CIM literal found outside designated SSOT files." >&2
|
|
35
|
+
echo "Use Opencdd::PropertyIds::REGISTRY / Opencdd::MetaClasses::REGISTRY instead." >&2
|
|
36
|
+
echo "" >&2
|
|
37
|
+
echo "$hits" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
exit 0
|
data/lib/cdd.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Back-compat shim. The canonical entry is `require "opencdd"`.
|
|
4
|
+
# `require "cdd"` continues to work for callers that haven't been
|
|
5
|
+
# updated yet, but new code should require "opencdd" directly.
|
|
6
|
+
require "opencdd"
|
|
7
|
+
|
|
8
|
+
# Alias for backward compatibility. The module was renamed from
|
|
9
|
+
# Cdd to Opencdd, but many callers (cdd-data Rakefile, specs, etc.)
|
|
10
|
+
# still reference Cdd::. This alias lets them keep working.
|
|
11
|
+
Cdd = Opencdd unless defined?(Cdd)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
class AliasTable
|
|
5
|
+
DUPLICATE_ALIAS = "duplicate alias: %<name>s"
|
|
6
|
+
|
|
7
|
+
def initialize(defaults: true)
|
|
8
|
+
@table = defaults ? PropertyIds.alias_map.dup : {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def declare(alias_name, property_id)
|
|
12
|
+
name = alias_name.to_s
|
|
13
|
+
existing = @table[name]
|
|
14
|
+
if existing
|
|
15
|
+
if existing == property_id.to_s
|
|
16
|
+
return self
|
|
17
|
+
end
|
|
18
|
+
raise ArgumentError, format(DUPLICATE_ALIAS, name: name)
|
|
19
|
+
end
|
|
20
|
+
raise ArgumentError, "unknown property id: #{property_id}" unless PropertyIds.entry(property_id)
|
|
21
|
+
@table[name] = property_id.to_s
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def redeclare(alias_name, property_id)
|
|
26
|
+
name = alias_name.to_s
|
|
27
|
+
raise ArgumentError, "unknown property id: #{property_id}" unless PropertyIds.entry(property_id)
|
|
28
|
+
@table[name] = property_id.to_s
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve(name)
|
|
33
|
+
@table[name.to_s]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def key?(name)
|
|
37
|
+
@table.key?(name.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def each(&block)
|
|
41
|
+
@table.each(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_h
|
|
45
|
+
@table.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def size
|
|
49
|
+
@table.size
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opencdd
|
|
4
|
+
module Cddal
|
|
5
|
+
module AST
|
|
6
|
+
Document = Struct.new(:declarations, keyword_init: true) do
|
|
7
|
+
def each_declaration
|
|
8
|
+
return enum_for(:each_declaration) unless block_given?
|
|
9
|
+
declarations.each { |d| yield d }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def meta_class_declarations
|
|
13
|
+
declarations.select { |d| d.is_a?(MetaClassDecl) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def instance_declarations
|
|
17
|
+
declarations.select { |d| d.is_a?(InstanceDecl) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def alias_declarations
|
|
21
|
+
declarations.select { |d| d.is_a?(AliasDecl) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def import_declarations
|
|
25
|
+
declarations.select { |d| d.is_a?(ImportDecl) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
MetaClassDecl = Struct.new(:irdi, :property_identifiers, :line, keyword_init: true) do
|
|
30
|
+
def property_ids
|
|
31
|
+
property_identifiers.map(&:to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
InstanceDecl = Struct.new(:name, :meta_class_ref, :assignments, :line, keyword_init: true)
|
|
36
|
+
|
|
37
|
+
AliasDecl = Struct.new(:alias_name, :property_id, :line, keyword_init: true)
|
|
38
|
+
|
|
39
|
+
# One name in a selective import (`from "x" import { Foo, Bar as B }`).
|
|
40
|
+
# +as+ is the optional local rename; when nil, the name is bound
|
|
41
|
+
# under its original form.
|
|
42
|
+
ImportedName = Struct.new(:name, :as, keyword_init: true) do
|
|
43
|
+
def local_name
|
|
44
|
+
as || name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# CDDAL module import declaration. Three shapes, discriminated
|
|
49
|
+
# by +kind+:
|
|
50
|
+
#
|
|
51
|
+
# :bare — textual inclusion of every named declaration
|
|
52
|
+
# in the target. +specifier+ is the path/URL.
|
|
53
|
+
# :qualified — target's names are accessible as
|
|
54
|
+
# +<qualifier>.<name>+. +qualifier+ is the alias.
|
|
55
|
+
# :selective — only the names in +imported_names+ are pulled
|
|
56
|
+
# in, each subject to its optional rename.
|
|
57
|
+
ImportDecl = Struct.new(
|
|
58
|
+
:specifier, :kind, :qualifier, :imported_names, :line,
|
|
59
|
+
keyword_init: true,
|
|
60
|
+
) do
|
|
61
|
+
def self.bare(specifier, line:)
|
|
62
|
+
new(specifier: specifier, kind: :bare, line: line)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.qualified(specifier, qualifier:, line:)
|
|
66
|
+
new(specifier: specifier, kind: :qualified, qualifier: qualifier, line: line)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.selective(specifier, imported_names:, line:)
|
|
70
|
+
new(specifier: specifier, kind: :selective, imported_names: imported_names, line: line)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Backward-compat accessor for callers that expect +url+.
|
|
74
|
+
def url
|
|
75
|
+
specifier
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
PropertyAssignment = Struct.new(:identifier, :language_tag, :value, :line, keyword_init: true) do
|
|
80
|
+
def resolved_key
|
|
81
|
+
language_tag ? "#{identifier}.#{language_tag}" : identifier
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Literal = Struct.new(:kind, :raw, keyword_init: true) do
|
|
86
|
+
def value
|
|
87
|
+
case kind
|
|
88
|
+
when :number then raw.match?(/\A-?\d+\z/) ? raw.to_i : raw.to_f
|
|
89
|
+
when :boolean then raw == "true"
|
|
90
|
+
when :null then nil
|
|
91
|
+
else raw
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_cddal
|
|
96
|
+
case kind
|
|
97
|
+
when :string then "\"#{escape(raw)}\""
|
|
98
|
+
when :boolean, :null then raw
|
|
99
|
+
else raw
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def escape(s)
|
|
106
|
+
s.to_s.gsub("\\", "\\\\").gsub('"', "\\\"")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
IdentifierRef = Struct.new(:name, :owner, keyword_init: true) do
|
|
111
|
+
def qualified?
|
|
112
|
+
!owner.nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_s
|
|
116
|
+
qualified? ? "#{owner}.#{name}" : name
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
alias_method :to_cddal, :to_s
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Set = Struct.new(:elements, keyword_init: true) do
|
|
123
|
+
def identifiers
|
|
124
|
+
elements
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_cddal
|
|
128
|
+
"{ #{elements.map(&:to_cddal).join(', ')} }"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Tuple = Struct.new(:elements, keyword_init: true) do
|
|
133
|
+
def to_cddal
|
|
134
|
+
"(#{elements.map(&:to_cddal).join(', ')})"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
ClassReference = Struct.new(:type_name, :argument, keyword_init: true) do
|
|
139
|
+
def to_cddal
|
|
140
|
+
"#{type_name}(#{argument})"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
Condition = Struct.new(:left, :operator, :right, keyword_init: true) do
|
|
145
|
+
def to_cddal
|
|
146
|
+
"#{left} #{operator} #{right.to_cddal}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|