edoxen 0.1.1 → 0.3.0

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.
data/README.adoc CHANGED
@@ -1,258 +1,202 @@
1
1
  = Edoxen
2
2
 
3
3
  https://github.com/metanorma/edoxen[image:https://img.shields.io/github/stars/metanorma/edoxen.svg?style=social[GitHub Stars]]
4
- https://github.com/metanorma/edoxen[image:https://img.shields.io/github/forks/metanorma/edoxen.svg?style=social[GitHub Forks]]
5
4
  image:https://img.shields.io/github/license/metanorma/edoxen.svg[License]
6
5
  image:https://img.shields.io/github/actions/workflow/status/metanorma/edoxen/test.yml?branch=main[Build Status]
7
6
  image:https://img.shields.io/gem/v/edoxen.svg[RubyGems Version]
8
7
 
9
8
  == Purpose
10
9
 
11
- Edoxen is a set of information models used for representing resolution and
12
- decision information. They are designed to provide a structured way to model
13
- formal resolutions, including their metadata, actions, considerations, and
14
- approvals.
10
+ Edoxen is a Ruby library for the canonical Edoxen information model of
11
+ formal resolutions and decisions. It is built on top of the
12
+ https://github.com/lutaml/lutaml[ lutaml-model] serialization framework.
15
13
 
16
- The `edoxen` gem provides a Ruby library for working with these models,
17
- allowing users to create, manipulate, and serialize resolution data in a
18
- structured format. It is built on top of the `lutaml-model` serialization
19
- framework, which provides a flexible and extensible way to define data models
20
- and serialize them to YAML or JSON formats.
14
+ The information model is defined in
15
+ https://github.com/metanorma/edoxen-model/tree/main/models[ LutaML UML
16
+ files] (one `.lutaml` per concept). This gem mirrors that model exactly —
17
+ attribute declarations, enum values, and field shapes so that anything
18
+ you can express in LutaML you can also construct, serialize, and validate
19
+ in Ruby.
21
20
 
22
- This library is particularly useful for standards organizations, committees,
23
- and governance bodies that need to maintain structured records of their
24
- decision-making processes.
25
-
26
-
27
- == Origin
28
-
29
- "Edoxen" is how all resolutions of Ancient Athens started.
30
-
31
- ____
32
- "It was the opinion of... (the people and city that...)"
33
- ____
34
-
35
- The word "edoxen" originates from the Ancient Greek word edokeō (ἔδοξεν),
36
- meaning "it was the opinion of" or "it seemed good to". This term was used in
37
- the context of formal resolutions and decisions made by the Athenian assembly,
38
- reflecting the collective will and judgment of the citizens.
39
-
40
-
41
- == Features
42
-
43
- * Classes for modeling resolutions, actions, considerations, and approvals
44
- * Support for resolution collections with metadata
45
- * YAML and JSON serialization with round-trip compatibility
46
- * Structured identifiers and meeting information
47
- * Resolution relationships and dependencies
48
- * Integration with the `lutaml-model` serialization framework
49
- * Comprehensive YAML schema for validation
50
- * Real-world data compatibility with ISO/TC 154 resolution formats
21
+ The intended users are standards organizations and governance bodies that
22
+ need to publish structured records of formal decision-making. Real-world
23
+ samples (ISO/TC 154, CIPM, OIML CIML) ship in `spec/fixtures/`.
51
24
 
52
25
  == Installation
53
26
 
54
- Add this line to your application's Gemfile:
55
-
56
27
  [source,ruby]
57
28
  ----
58
29
  gem 'edoxen'
59
30
  ----
60
31
 
61
- And then execute:
62
-
63
- [source,sh]
64
- ----
65
- $ bundle install
66
- ----
67
-
68
- Or install it yourself as:
69
-
70
- [source,sh]
71
- ----
72
- $ gem install edoxen
73
- ----
32
+ Then `bundle install`, or `gem install edoxen` standalone.
74
33
 
75
34
  == Quick start
76
35
 
77
- Here's a minimal example to get you started:
78
-
79
36
  [source,ruby]
80
37
  ----
81
38
  require 'edoxen'
82
39
 
83
- # Create a simple resolution
84
- resolution = Edoxen::Resolution.new(
85
- title: "Adoption of new standard",
86
- type: "resolution",
87
- category: "Technical resolutions",
88
- identifier: "2024-01"
89
- )
90
-
91
- # Add an action to the resolution
92
- action = Edoxen::Action.new(
93
- type: "resolves",
94
- message: "to adopt ISO 12345 as a new standard",
95
- date_effective: Date.new(2024, 1, 15)
96
- )
97
- resolution.actions = [action]
98
-
99
- # Add a consideration
100
- consideration = Edoxen::Consideration.new(
101
- type: "having",
102
- message: "reviewed the technical specifications"
103
- )
104
- resolution.considerations = [consideration]
105
-
106
- # Serialize to YAML
107
- puts resolution.to_yaml
108
-
109
- # Create a resolution collection
110
- collection = Edoxen::ResolutionCollection.new(
111
- metadata: {
112
- title: "Resolutions of the 42nd plenary meeting",
113
- date: "2024-01-15",
114
- source: "ISO/TC 154 Secretariat"
115
- },
116
- resolutions: [resolution]
117
- )
118
-
119
- # Serialize collection to YAML
120
- puts collection.to_yaml
121
- ----
122
-
123
- == Documentation
124
-
125
- === Data models
126
-
127
- The library provides these main model classes:
128
-
129
- `Edoxen::Resolution`::
130
- Core model representing a formal resolution with title, type, category, and
131
- associated elements.
40
+ yaml = File.read('resolutions.yaml')
41
+ collection = Edoxen::ResolutionCollection.from_yaml(yaml)
132
42
 
133
- `Edoxen::ResolutionCollection`::
134
- Container for multiple resolutions with metadata about the meeting or
135
- collection context.
43
+ collection.resolutions.each do |resolution|
44
+ prefix = resolution.identifier.first.prefix
45
+ number = resolution.identifier.first.number
46
+ puts "#{prefix}/#{number}"
136
47
 
137
- `Edoxen::Action`::
138
- Represents actions taken within a resolution (e.g., "resolves", "requests",
139
- "thanks").
48
+ resolution.localizations.each do |loc|
49
+ puts " [#{loc.language_code}/#{loc.script}] #{loc.title}"
50
+ loc.actions.each do |action|
51
+ puts " - #{action.type}: #{action.message}"
52
+ end
53
+ end
54
+ end
140
55
 
141
- `Edoxen::Consideration`::
142
- Models considerations that lead to a resolution (e.g., "having", "noting",
143
- "recognizing").
144
-
145
- `Edoxen::Approval`::
146
- Captures approval information including type and degree of consensus.
147
-
148
- `Edoxen::MeetingIdentifier`::
149
- Contains meeting identification information such as venue, date, and identifier.
150
-
151
- `Edoxen::ResolutionRelationship`::
152
- Defines relationships between resolutions (e.g., "supersedes", "amends").
153
-
154
- === Architecture overview
155
-
156
- The library is built on `lutaml-model` and follows these design principles:
157
-
158
- **Model-based approach**::
159
- Each entity is represented as a Ruby class with defined attributes and
160
- serialization mappings.
161
-
162
- **Flexible serialization**::
163
- Support for both YAML and JSON formats with automatic type conversion and
164
- round-trip compatibility.
165
-
166
- **Extensible structure**::
167
- Models allow additional properties beyond the core schema, enabling
168
- customization for specific organizational needs.
169
-
170
- **Real-world compatibility**::
171
- Designed to work with existing resolution data formats used by standards
172
- organizations.
173
-
174
- == Usage workflow
175
-
176
- The `edoxen` workflow follows a straightforward approach:
177
-
178
- === 1. Data modeling
56
+ # Round-trip back to YAML
57
+ puts collection.to_yaml
58
+ ----
179
59
 
180
- . **Create resolutions**: Instantiate `Resolution` objects with required
181
- attributes
182
- . **Add components**: Attach actions, considerations, approvals as needed
183
- . **Build collections**: Group resolutions into collections with metadata
60
+ == Data model
184
61
 
185
- === 2. Serialization and persistence
62
+ The full model is in `lib/edoxen/*.rb` (Ruby) and `schema/edoxen.yaml`
63
+ (JSON-Schema). Both are kept in lockstep by
64
+ `spec/edoxen/schema_enum_sync_spec.rb`.
186
65
 
187
- . **Export to YAML/JSON**: Use built-in serialization methods
188
- . **Validate structure**: Leverage the provided YAML schema for validation
189
- . **Round-trip processing**: Load and save data while preserving structure
66
+ [source]
67
+ ----
68
+ ResolutionCollection
69
+ ├── metadata: ResolutionMetadata
70
+ │ ├── title / title_localized[]
71
+ │ ├── date
72
+ │ ├── source
73
+ │ ├── source_urls: SourceUrl[]
74
+ │ ├── city, country_code
75
+ └── resolutions: Resolution[]
76
+ ├── identifier: StructuredIdentifier[1..*]
77
+ ├── type: ResolutionType (resolution | recommendation | decision | declaration)
78
+ ├── doi, urn, agenda_item
79
+ ├── dates: ResolutionDate[]
80
+ ├── categories: String[]
81
+ ├── meeting: MeetingIdentifier
82
+ ├── relations: ResolutionRelation[]
83
+ ├── urls: Url[]
84
+ └── localizations: Localization[1..*]
85
+ ├── language_code (ISO 639-3)
86
+ ├── script (ISO 15924)
87
+ ├── title, subject, message, considering
88
+ ├── considerations: Consideration[]
89
+ ├── approvals: Approval[]
90
+ └── actions: Action[]
91
+ ----
190
92
 
191
- == YAML format specification
93
+ Every translatable field on a Resolution is wrapped inside one of its
94
+ `localizations[]` entries. Language-agnostic admin fields
95
+ (`identifier`, `doi`, `urn`, `agenda_item`, `dates`, `categories`,
96
+ `meeting`, `relations`, `urls`) live on the parent `Resolution`.
192
97
 
193
- Edoxen uses a structured YAML format for resolution data. Here's an example:
98
+ == Multilingual resolution sets
194
99
 
195
100
  [source,yaml]
196
101
  ----
197
- metadata:
198
- title: "Resolutions of the 42nd plenary meeting of ISO/TC 154"
199
- date: "2024-01-15"
200
- source: "ISO/TC 154 Secretariat"
201
- venue: "Virtual meeting"
202
-
203
102
  resolutions:
204
- - identifier: "2024-01"
205
- title: "Adoption of new standard"
206
- type: "resolution"
207
- category: "Technical resolutions"
208
-
209
- considerations:
210
- - type: "having"
211
- message: "reviewed the technical specifications"
212
-
213
- actions:
214
- - type: "resolves"
215
- message: "to adopt ISO 12345 as a new standard"
216
- date_effective: "2024-01-15"
217
-
218
- approvals:
219
- - type: "affirmative"
220
- degree: "unanimous"
103
+ - identifier:
104
+ - prefix: CIML
105
+ number: "2025-44"
106
+ doi: 10.63493/resolutions/ciml202544
107
+ agenda_item: "16.2"
108
+ dates:
109
+ - date: 2025-10-13
110
+ type: discussed
111
+ localizations:
112
+ - language_code: eng
113
+ script: Latn
114
+ title: Decision on the renewal of the contract of Mr Anthony Donnellan
115
+ subject: CIML
116
+ actions:
117
+ - type: decides
118
+ date_effective:
119
+ date: 2025-10-13
120
+ type: adoption
121
+ message: |
122
+ The Committee decides to renew the contract of
123
+ Mr Anthony Donnellan as BIML Director.
124
+ - language_code: fra
125
+ script: Latn
126
+ title: Décision sur le renouvellement du contrat de M. Anthony Donnellan
127
+ subject: CIML
128
+ actions:
129
+ - type: decides
130
+ date_effective:
131
+ date: 2025-10-13
132
+ type: adoption
133
+ message: |
134
+ Le Comité décide de renouveler le contrat de
135
+ M. Anthony Donnellan en tant que Directeur du BIML.
221
136
  ----
222
137
 
223
- === Schema validation
138
+ == Command-line interface
139
+
140
+ The `edoxen` executable exposes two commands.
141
+
142
+ === `validate` — JSON-Schema validation + model parsing
224
143
 
225
- A comprehensive YAML schema is provided at `schema/edoxen.yaml` for validating
226
- resolution data files. The schema follows JSON Schema Draft 7 conventions and
227
- includes:
144
+ [source,sh]
145
+ ----
146
+ $ edoxen validate "spec/fixtures/*.yaml"
147
+ ----
228
148
 
229
- * Complete structure definitions for all model types
230
- * Enumerated values for type validation
231
- * Date format validation
232
- * Flexible extensibility with `additionalProperties`
149
+ Runs both `Edoxen::SchemaValidator` and `Edoxen::ResolutionCollection.from_yaml`
150
+ against each matching file. The schema catches `additionalProperties`,
151
+ `required`, enum, and pattern violations; the model catches structural
152
+ problems the schema can't express.
233
153
 
234
- The schema can be used with standard JSON Schema validators to ensure data
235
- integrity and compliance.
154
+ Exit code is non-zero if any file fails.
236
155
 
237
- == Real-world examples
156
+ === `normalize` — round-trip every fixture through the model
238
157
 
239
- The library has been tested with real resolution data from ISO/TC 154 and
240
- other standards organizations. It successfully handles:
158
+ [source,sh]
159
+ ----
160
+ $ edoxen normalize "spec/fixtures/*.yaml" --output clean/
161
+ $ edoxen normalize legacy.yaml --inplace
162
+ ----
241
163
 
242
- * Complex resolution structures with multiple actions and considerations
243
- * Various resolution types (resolutions, recommendations, decisions)
244
- * Meeting metadata and organizational information
245
- * Cross-references and relationships between resolutions
246
- * Historical data migration and format conversion
164
+ Loads each YAML file, parses it through the Ruby model, and writes the
165
+ result back. Used to migrate between model versions. Either `--output DIR`
166
+ or `--inplace` is required (mutually exclusive).
167
+
168
+ == Architecture
169
+
170
+ * `lib/edoxen.rb` is the single entry-point. It configures
171
+ `Lutaml::Model::Config` and `autoload`s every model class and service.
172
+ * Each model lives in its own file under `lib/edoxen/`. No `require_relative`
173
+ — all cross-references resolve through Ruby autoload.
174
+ * `lib/edoxen/enums.rb` is the single source of truth for every enum
175
+ value used by the gem. Both the Ruby model (`attribute :type, :string,
176
+ values: Enums::ACTION_TYPE`) and `schema/edoxen.yaml` refer to the
177
+ same constant; `spec/edoxen/schema_enum_sync_spec.rb` asserts equality
178
+ at runtime.
179
+ * `lib/edoxen/schema_validator.rb` is intentionally small: a
180
+ `SchemaValidator::ValidationError`, a `validate_file` method, a
181
+ `validate_content` method, and a `LineMap` module for line-accurate
182
+ error reporting. Adding new collection paths never requires touching
183
+ it — the lookup is longest-prefix match against an indent-heuristic
184
+ line map.
185
+ * `lib/edoxen/cli.rb` is the Thor surface that glues SchemaValidator and
186
+ `ResolutionCollection.from_yaml` together for the two CLI commands.
247
187
 
248
188
  == Contributing
249
189
 
250
- Bug reports and pull requests are welcome on GitHub at
251
- https://github.com/metanorma/edoxen.
190
+ Follow the rules in `CLAUDE.md`:
252
191
 
253
- == License and Copyright
192
+ * All public methods have specs.
193
+ * Specs use real model instances — never `double()`.
194
+ * Serialization goes through `lutaml-model` only — no hand-rolled
195
+ `to_h`, `from_h`, `to_yaml`, or `to_json` on a model class.
196
+ * Schema and Ruby enum values must agree; the schema_sync_spec catches
197
+ drift.
198
+ * All changes go through PRs. Never commit to `main`, never push tags.
254
199
 
255
- This project is licensed under the BSD 2-clause License.
256
- See the link:LICENSE[] file for details.
200
+ == License
257
201
 
258
- Copyright Ribose.
202
+ BSD-2-Clause. Copyright Ribose Inc.
data/edoxen.gemspec CHANGED
@@ -10,7 +10,11 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Edoxen is a set of information models used for representing resolution and decision information."
12
12
  spec.description = <<~HEREDOC
13
- Edoxen provides a Ruby library for working with resolution models, allowing users to create, manipulate, and serialize resolution data in a structured format. It is built on top of the lutaml-model serialization framework, which provides a flexible and extensible way to define data models and serialize them to YAML or JSON formats.
13
+ Edoxen provides a Ruby library for working with resolution models, allowing
14
+ users to create, manipulate, and serialize resolution data in a structured
15
+ format. It is built on top of the lutaml-model serialization framework,
16
+ which provides a flexible and extensible way to define data models and
17
+ serialize them to YAML or JSON formats.
14
18
  HEREDOC
15
19
 
16
20
  spec.homepage = "https://github.com/metanorma/edoxen"
@@ -32,5 +36,7 @@ Gem::Specification.new do |spec|
32
36
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
37
  spec.require_paths = ["lib"]
34
38
 
39
+ spec.add_dependency "json_schemer", "~> 2.0"
35
40
  spec.add_dependency "lutaml-model", "~> 0.7"
41
+ spec.add_dependency "thor", "~> 1.0"
36
42
  end
data/exe/edoxen ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/edoxen"
5
+
6
+ Edoxen::Cli.start(ARGV)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "resolution_date"
5
+ require_relative "url"
6
+ require_relative "localization"
7
+
8
+ module Edoxen
9
+ # A per-language source-URL record. Carries the URL ref, its format
10
+ # (pdf, html, ...), and the language_code (ISO 639-3) for which the
11
+ # URL is the canonical source.
12
+ class SourceUrl < Lutaml::Model::Serializable
13
+ attribute :ref, :string
14
+ attribute :format, :string
15
+ attribute :language_code, :string
16
+
17
+ key_value do
18
+ map "ref", to: :ref
19
+ map "format", to: :format
20
+ map "language_code", to: :language_code
21
+ end
22
+ end
23
+
24
+ class Metadata < Lutaml::Model::Serializable
25
+ attribute :title, :string
26
+ attribute :identifier, :string
27
+ attribute :dates, ResolutionDate, collection: true
28
+ attribute :source, :string
29
+ attribute :venue, :string
30
+ attribute :chair, :string
31
+ attribute :urls, Url, collection: true
32
+
33
+ # OIML extensions — see TODO.complete/14 for the glossarist-style
34
+ # i18n model. `title_localized` carries the per-language title
35
+ # parallel to Resolution#localizations. `source_urls` carries the
36
+ # per-language PDF URLs. `city`/`country_code` carry the IATA /
37
+ # ISO 3166-1 alpha-2 codes for the host venue.
38
+ attribute :title_localized, Localization, collection: true
39
+ attribute :source_urls, SourceUrl, collection: true
40
+ attribute :city, :string
41
+ attribute :country_code, :string
42
+
43
+ key_value do
44
+ map "title", to: :title
45
+ map "identifier", to: :identifier
46
+ map "dates", to: :dates
47
+ map "source", to: :source
48
+ map "venue", to: :venue
49
+ map "chair", to: :chair
50
+ map "urls", to: :urls
51
+ map "title_localized", to: :title_localized
52
+ map "source_urls", to: :source_urls
53
+ map "city", to: :city
54
+ map "country_code", to: :country_code
55
+ end
56
+ end
57
+ end
data/lib/edoxen/action.rb CHANGED
@@ -1,65 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Edoxen
4
+ # Verb + one effective date + human-readable message. Used inside a
5
+ # Localization to express the multilingual part of an action.
6
6
  class Action < Lutaml::Model::Serializable
7
- ACTION_TYPE_ENUM = %w[
8
- adopts thanks approves decides declares asks invites resolves confirms
9
- welcomes recommends requests congratulates instructs urges appoints
10
- resolves further instructs calls-upon encourages affirms elects
11
- authorizes charges states remarks judges sanctions abrogates empowers
12
- ].freeze
13
-
14
- attribute :type, :string, values: ACTION_TYPE_ENUM
15
- attribute :date_effective, :date
7
+ attribute :type, :string, values: Enums::ACTION_TYPE
8
+ attribute :date_effective, ResolutionDate
16
9
  attribute :message, :string
17
- attribute :subject, :string
18
10
 
19
11
  key_value do
20
12
  map "type", to: :type
21
13
  map "date_effective", to: :date_effective
22
14
  map "message", to: :message
23
- map "subject", to: :subject
24
15
  end
25
16
  end
26
17
  end
27
-
28
- # Action {
29
- # type: ActionType
30
- # dateEffective: Date
31
- # message: Text
32
- # }
33
-
34
- # enum ActionType {
35
- # adopts
36
- # thanks / expresses-appreciation (subjects)
37
- # approves
38
- # decides
39
- # declares
40
- # asks (subjects)
41
- # invites / further invites (subjects)
42
- # resolves
43
- # confirms
44
- # welcomes (subjects)
45
- # recommends
46
- # requests (subjects)
47
- # congratulates (subjects)
48
- # instructs (subjects)
49
- # urges (subjects)
50
- # appoints (subjects)
51
- # resolves further
52
- # instructs (subjects)
53
- # calls upon (subjects)
54
- # encourages (subjects)
55
- # affirms / reaffirming (subjects)
56
- # elects
57
- # authorizes
58
- # charges
59
- # states
60
- # remarks
61
- # judges
62
- # sanctions
63
- # abrogates
64
- # empowers
65
- # }
@@ -1,32 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Approval {
4
- # type: ApprovalType
5
- # degree: ApprovalDegree
6
- # date: Date
7
- # message: Text
8
- # }
9
-
10
- # enum ApprovalType {
11
- # affirmative
12
- # negative
13
- # }
14
-
15
- # enum ApprovalDegree {
16
- # unanimous
17
- # majority
18
- # minority
19
- # }
20
- require "lutaml/model"
21
-
22
3
  module Edoxen
4
+ # Approval record: type (affirmative / negative), degree (consensus level),
5
+ # date of the approval event, and a human-readable elaboration.
23
6
  class Approval < Lutaml::Model::Serializable
24
- APPROVAL_TYPE_ENUM = %w[affirmative negative].freeze
25
- APPROVAL_DEGREE_ENUM = %w[unanimous majority minority].freeze
26
-
27
- attribute :type, :string, values: APPROVAL_TYPE_ENUM
28
- attribute :degree, :string, values: APPROVAL_DEGREE_ENUM
29
- attribute :date, :date
7
+ attribute :type, :string, values: Enums::APPROVAL_TYPE
8
+ attribute :degree, :string, values: Enums::APPROVAL_DEGREE
9
+ attribute :date, ResolutionDate
30
10
  attribute :message, :string
31
11
 
32
12
  key_value do