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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +486 -0
  3. data/README.md +304 -0
  4. data/bin/lint-no-raw-mdc +41 -0
  5. data/lib/cdd.rb +11 -0
  6. data/lib/opencdd/alias_table.rb +52 -0
  7. data/lib/opencdd/cddal/ast.rb +151 -0
  8. data/lib/opencdd/cddal/builder.rb +374 -0
  9. data/lib/opencdd/cddal/fetcher/in_memory.rb +32 -0
  10. data/lib/opencdd/cddal/fetcher/net_http.rb +84 -0
  11. data/lib/opencdd/cddal/fetcher.rb +18 -0
  12. data/lib/opencdd/cddal/generated_parser.rb +805 -0
  13. data/lib/opencdd/cddal/lexer.rb +193 -0
  14. data/lib/opencdd/cddal/parser.rb +19 -0
  15. data/lib/opencdd/cddal/resolver.rb +100 -0
  16. data/lib/opencdd/cddal/serializer.rb +210 -0
  17. data/lib/opencdd/cddal/value_serializer.rb +60 -0
  18. data/lib/opencdd/cddal.rb +63 -0
  19. data/lib/opencdd/class_tree.rb +80 -0
  20. data/lib/opencdd/class_type.rb +33 -0
  21. data/lib/opencdd/codegen/ts.rb +185 -0
  22. data/lib/opencdd/codegen.rb +7 -0
  23. data/lib/opencdd/composition_tree.rb +119 -0
  24. data/lib/opencdd/condition.rb +120 -0
  25. data/lib/opencdd/data_type.rb +143 -0
  26. data/lib/opencdd/database.rb +719 -0
  27. data/lib/opencdd/effective_properties.rb +119 -0
  28. data/lib/opencdd/entity/field_reader.rb +141 -0
  29. data/lib/opencdd/entity/field_registry.rb +99 -0
  30. data/lib/opencdd/entity/version_history.rb +62 -0
  31. data/lib/opencdd/entity.rb +255 -0
  32. data/lib/opencdd/exporters/json.rb +235 -0
  33. data/lib/opencdd/exporters/mermaid.rb +62 -0
  34. data/lib/opencdd/exporters/yaml.rb +16 -0
  35. data/lib/opencdd/exporters.rb +9 -0
  36. data/lib/opencdd/guid.rb +27 -0
  37. data/lib/opencdd/instance_rule.rb +70 -0
  38. data/lib/opencdd/irdi.rb +128 -0
  39. data/lib/opencdd/klass.rb +230 -0
  40. data/lib/opencdd/languages.rb +70 -0
  41. data/lib/opencdd/meta_class.rb +274 -0
  42. data/lib/opencdd/model/entity_store.rb +85 -0
  43. data/lib/opencdd/model/yaml_database.rb +49 -0
  44. data/lib/opencdd/model/yaml_entity.rb +196 -0
  45. data/lib/opencdd/model.rb +13 -0
  46. data/lib/opencdd/parcel/csv_reader.rb +35 -0
  47. data/lib/opencdd/parcel/csv_writer.rb +114 -0
  48. data/lib/opencdd/parcel/entity_manifest.rb +119 -0
  49. data/lib/opencdd/parcel/flat_dir_reader.rb +134 -0
  50. data/lib/opencdd/parcel/layout_detector.rb +115 -0
  51. data/lib/opencdd/parcel/metadata.rb +112 -0
  52. data/lib/opencdd/parcel/referenced_irdis.rb +91 -0
  53. data/lib/opencdd/parcel/scrape_verifier.rb +122 -0
  54. data/lib/opencdd/parcel/selector.rb +115 -0
  55. data/lib/opencdd/parcel/sharded_dir_reader.rb +151 -0
  56. data/lib/opencdd/parcel/sheet.rb +287 -0
  57. data/lib/opencdd/parcel/sheet_emitter.rb +171 -0
  58. data/lib/opencdd/parcel/sheet_schema.rb +253 -0
  59. data/lib/opencdd/parcel/workbook.rb +172 -0
  60. data/lib/opencdd/parcel/workbook_reader.rb +200 -0
  61. data/lib/opencdd/parcel/writer.rb +185 -0
  62. data/lib/opencdd/parcel.rb +175 -0
  63. data/lib/opencdd/parse_helpers.rb +73 -0
  64. data/lib/opencdd/property.rb +120 -0
  65. data/lib/opencdd/property_data_element_type.rb +44 -0
  66. data/lib/opencdd/property_ids.rb +202 -0
  67. data/lib/opencdd/reader.rb +103 -0
  68. data/lib/opencdd/relation.rb +88 -0
  69. data/lib/opencdd/relation_tree.rb +74 -0
  70. data/lib/opencdd/relation_type.rb +58 -0
  71. data/lib/opencdd/structured_values.rb +183 -0
  72. data/lib/opencdd/unit.rb +16 -0
  73. data/lib/opencdd/validator/class_reference_rule.rb +77 -0
  74. data/lib/opencdd/validator/condition_rule.rb +27 -0
  75. data/lib/opencdd/validator/data_type_rule.rb +26 -0
  76. data/lib/opencdd/validator/enum_rule.rb +27 -0
  77. data/lib/opencdd/validator/format_rule.rb +54 -0
  78. data/lib/opencdd/validator/hierarchy_rule.rb +65 -0
  79. data/lib/opencdd/validator/irdi_rule.rb +57 -0
  80. data/lib/opencdd/validator/mandatory_rule.rb +25 -0
  81. data/lib/opencdd/validator/pattern_rule.rb +26 -0
  82. data/lib/opencdd/validator/reference_rule.rb +36 -0
  83. data/lib/opencdd/validator/rule.rb +65 -0
  84. data/lib/opencdd/validator/runner.rb +101 -0
  85. data/lib/opencdd/validator/set_rule.rb +41 -0
  86. data/lib/opencdd/validator/synonym_rule.rb +30 -0
  87. data/lib/opencdd/validator/type_rule.rb +102 -0
  88. data/lib/opencdd/validator/uniqueness_rule.rb +32 -0
  89. data/lib/opencdd/validator.rb +68 -0
  90. data/lib/opencdd/value_format.rb +71 -0
  91. data/lib/opencdd/value_list.rb +42 -0
  92. data/lib/opencdd/value_term.rb +24 -0
  93. data/lib/opencdd/version.rb +5 -0
  94. data/lib/opencdd/view_control.rb +10 -0
  95. data/lib/opencdd/visitor.rb +93 -0
  96. data/lib/opencdd.rb +57 -0
  97. metadata +325 -0
data/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # opencdd — Ruby model for the IEC Common Data Dictionary
2
+
3
+ [![CI](https://github.com/opencdd/opencdd-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/opencdd/opencdd-ruby/actions/workflows/ci.yml)
4
+ [![Gem](https://img.shields.io/gem/v/opencdd.svg)](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.
@@ -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