unidpp 0.1.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.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Unidpp
7
+ module Registry
8
+ # Local JSON/YAML store adapter: registry content read from a
9
+ # directory or a single consolidated document. All (de)serialization
10
+ # of items, bindings and register metadata goes through lutaml-model
11
+ # (Item.from_yaml / from_json, etc.); this adapter only locates and
12
+ # reads raw documents.
13
+ #
14
+ # Directory layout (glossarist-dataset style, one item per file):
15
+ #
16
+ # <path>/register.yaml Register metadata
17
+ # <path>/items/*.yaml one Item per file
18
+ # <path>/bindings/*.yaml one ApplicabilityBinding or a
19
+ # bindings: [...] list per file
20
+ #
21
+ # Single-file layout (JSON or YAML): one document with the keys
22
+ # +register+, +items+, +applicability+.
23
+ #
24
+ # Item version entries are immutable per version and perfectly
25
+ # cacheable — only register status is mutable — so a FileStore may
26
+ # also serve as an offline mirror of a remote register.
27
+ class FileStore < Store
28
+ class FormatError < Unidpp::Error; end
29
+
30
+ ITEM_GLOB = "items/*.{yaml,yml,json}".freeze
31
+ BINDING_GLOB = "bindings/*.{yaml,yml,json}".freeze
32
+
33
+ # @param path [String] directory or single document file
34
+ def initialize(path)
35
+ @path = File.expand_path(path)
36
+ @directory = File.directory?(@path)
37
+ freeze_content
38
+ end
39
+
40
+ # @return [Array<Item>]
41
+ def items
42
+ content[:items]
43
+ end
44
+
45
+ # @return [Array<ApplicabilityBinding>]
46
+ def bindings
47
+ content[:bindings]
48
+ end
49
+
50
+ # @return [Register, nil]
51
+ def register
52
+ content[:register]
53
+ end
54
+
55
+ private
56
+
57
+ def content
58
+ @content
59
+ end
60
+
61
+ def freeze_content
62
+ @content =
63
+ if @directory
64
+ load_directory
65
+ else
66
+ load_single_file
67
+ end
68
+ end
69
+
70
+ def load_directory
71
+ {
72
+ register: load_register,
73
+ items: Dir[File.join(@path, ITEM_GLOB)].sort.map { |f| load_item(f) },
74
+ bindings: Dir[File.join(@path, BINDING_GLOB)].sort.flat_map { |f| load_bindings(f) },
75
+ }
76
+ end
77
+
78
+ def load_single_file
79
+ doc = read_document(@path)
80
+ unless doc.is_a?(Hash)
81
+ raise FormatError, "registry document must be a mapping: #{@path}"
82
+ end
83
+
84
+ {
85
+ register: doc["register"] && Register.from_hash(doc["register"]),
86
+ items: Array(doc["items"]).map { |i| Item.from_hash(i) },
87
+ bindings: Array(doc["applicability"] || doc["bindings"])
88
+ .map { |b| ApplicabilityBinding.from_hash(b) },
89
+ }
90
+ end
91
+
92
+ def load_register
93
+ path = Dir[File.join(@path, "register.{yaml,yml,json}")].first
94
+ doc = path && read_document(path)
95
+ doc && Register.from_hash(doc)
96
+ end
97
+
98
+ def load_item(path)
99
+ Item.from_hash(read_document(path))
100
+ end
101
+
102
+ def load_bindings(path)
103
+ doc = read_document(path)
104
+ list = doc.is_a?(Hash) ? doc["bindings"] : doc
105
+ Array(list).map { |b| ApplicabilityBinding.from_hash(b) }
106
+ end
107
+
108
+ # Reads a raw JSON or YAML document (plain data; YAML timestamps are
109
+ # permitted and cast to ISO strings by the lutaml DateTime type).
110
+ def read_document(path)
111
+ if File.extname(path) == ".json"
112
+ JSON.parse(File.read(path))
113
+ else
114
+ YAML.safe_load(File.read(path),
115
+ permitted_classes: [Time, Date, DateTime],
116
+ aliases: true)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ module Registry
5
+ # A registry item: the single definition (I2 — single definition, many
6
+ # constraints) of a data element, profile, transform, crypto suite,
7
+ # trust anchor or unit. Item references are (register, item, version).
8
+ # Profiles constrain and compose; they never redefine — where regions
9
+ # regulate differently they register distinct items.
10
+ #
11
+ # Profile items embed their ProfileManifest, version-pinned to one of
12
+ # the item's versions (seam S4: version pinning at the profile↔registry
13
+ # join).
14
+ class Item < Lutaml::Model::Serializable
15
+ ITEM_CLASSES = %w[
16
+ data-element profile crypto-suite transform trust-anchor unit
17
+ ].freeze
18
+
19
+ class UnknownVersionError < Unidpp::Error; end
20
+
21
+ attribute :identifier, :string
22
+ attribute :register, :string
23
+ attribute :item_class, :string, values: ITEM_CLASSES
24
+ attribute :title, :string
25
+ # ISO 19135 submitting organization for this item.
26
+ attribute :submitting_organization, :string
27
+ attribute :versions, ItemVersion, collection: true, default: -> { [] }
28
+ attribute :manifest, ProfileManifest
29
+
30
+ json do
31
+ map "identifier", to: :identifier
32
+ map "register", to: :register, render_nil: false
33
+ map "item_class", to: :item_class
34
+ map "title", to: :title, render_nil: false
35
+ map "submitting_organization", to: :submitting_organization, render_nil: false
36
+ map "versions", to: :versions
37
+ map "manifest", to: :manifest, render_nil: false
38
+ end
39
+
40
+ yaml do
41
+ map "identifier", to: :identifier
42
+ map "register", to: :register, render_nil: false
43
+ map "item_class", to: :item_class
44
+ map "title", to: :title, render_nil: false
45
+ map "submitting_organization", to: :submitting_organization, render_nil: false
46
+ map "versions", to: :versions
47
+ map "manifest", to: :manifest, render_nil: false
48
+ end
49
+
50
+ # item_class predicates (profile? / unit? / ...) are generated by
51
+ # lutaml-model from the item_class value enumeration.
52
+
53
+ # @param number [String]
54
+ # @return [ItemVersion, nil]
55
+ def version(number)
56
+ versions.find { |v| v.version == number.to_s }
57
+ end
58
+
59
+ # @param number [String]
60
+ # @return [ItemVersion]
61
+ # @raise [UnknownVersionError]
62
+ def version!(number)
63
+ version(number) ||
64
+ raise(UnknownVersionError, "#{identifier} has no version #{number}")
65
+ end
66
+
67
+ # Versions in registration order (by effective_from, then version).
68
+ #
69
+ # @return [Array<ItemVersion>]
70
+ def ordered_versions
71
+ versions.sort_by do |v|
72
+ [v.effective_from || DateTime.new(0), v.version.to_s]
73
+ end
74
+ end
75
+
76
+ # The version currently in force: latest effective version with
77
+ # status "valid".
78
+ #
79
+ # @return [ItemVersion, nil]
80
+ def current_version
81
+ reversed = ordered_versions.reverse
82
+ reversed.find(&:valid?) || reversed.find { |v| !v.retired? }
83
+ end
84
+
85
+ # Follows supersession links from the earliest version carrying one
86
+ # (or from +from+, when given) to the terminal version.
87
+ #
88
+ # @param from [String, nil] version number to start from
89
+ # @return [Array<ItemVersion>] the chain, in supersession order
90
+ def supersession_chain(from: nil)
91
+ start = from ? version!(from) : ordered_versions.first
92
+ chain = [start]
93
+ while (next_number = chain.last.superseded_by_version)
94
+ chain << version!(next_number)
95
+ end
96
+ chain
97
+ end
98
+
99
+ # Resolved effective window end: explicit effective_until, else the
100
+ # successor's effective_from, else open (nil).
101
+ #
102
+ # @param version_number [String]
103
+ # @return [DateTime, nil]
104
+ def window_until(version_number)
105
+ v = version!(version_number)
106
+ return v.effective_until if v.effective_until
107
+ return nil if v.superseded_by_version.nil?
108
+
109
+ version(v.superseded_by_version)&.effective_from
110
+ end
111
+
112
+ # The version in force at time +t+ (point-in-time registry state).
113
+ #
114
+ # @param t [Time, DateTime]
115
+ # @return [ItemVersion, nil]
116
+ def in_force_at(t)
117
+ t = t.is_a?(DateTime) ? t : DateTime.parse(t.to_s)
118
+ ordered_versions
119
+ .select { |v| v.in_force_at?(t, until_time: window_until(v.version)) }
120
+ .last
121
+ end
122
+
123
+ # Seam S4 discipline: an embedded manifest must be pinned to a
124
+ # version of this item.
125
+ #
126
+ # @return [Boolean]
127
+ def manifest_version_pinned?
128
+ manifest.nil? || !version(manifest.version).nil?
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ module Registry
5
+ # One version of a registry item with its ISO 19135 lifecycle status.
6
+ # Versions are immutable once registered; status and supersession
7
+ # links express the lifecycle (valid / superseded / retired).
8
+ class ItemVersion < Lutaml::Model::Serializable
9
+ STATUSES = %w[valid superseded retired].freeze
10
+
11
+ attribute :version, :string
12
+ attribute :status, :string, values: STATUSES
13
+ # Effective window of this version. An open effective_until is
14
+ # derived from the successor's effective_from when absent.
15
+ attribute :effective_from, "Lutaml::Model::Type::DateTime"
16
+ attribute :effective_until, "Lutaml::Model::Type::DateTime"
17
+ attribute :registered_at, "Lutaml::Model::Type::DateTime"
18
+ attribute :superseded_by_version, :string
19
+ attribute :notes, :string
20
+
21
+ json do
22
+ map "version", to: :version
23
+ map "status", to: :status
24
+ map "effective_from", to: :effective_from, render_nil: false
25
+ map "effective_until", to: :effective_until, render_nil: false
26
+ map "registered_at", to: :registered_at, render_nil: false
27
+ map "superseded_by_version", to: :superseded_by_version, render_nil: false
28
+ map "notes", to: :notes, render_nil: false
29
+ end
30
+
31
+ yaml do
32
+ map "version", to: :version
33
+ map "status", to: :status
34
+ map "effective_from", to: :effective_from, render_nil: false
35
+ map "effective_until", to: :effective_until, render_nil: false
36
+ map "registered_at", to: :registered_at, render_nil: false
37
+ map "superseded_by_version", to: :superseded_by_version, render_nil: false
38
+ map "notes", to: :notes, render_nil: false
39
+ end
40
+
41
+ def valid?
42
+ status == "valid"
43
+ end
44
+
45
+ def superseded?
46
+ status == "superseded"
47
+ end
48
+
49
+ def retired?
50
+ status == "retired"
51
+ end
52
+
53
+ # @param t [Time, DateTime]
54
+ # @param until_time [DateTime, nil] resolved window end (explicit or
55
+ # derived from the successor)
56
+ # @return [Boolean] this version was in force at +t+
57
+ def in_force_at?(t, until_time: effective_until)
58
+ t = t.is_a?(DateTime) ? t : DateTime.parse(t.to_s)
59
+ return false if effective_from && t < effective_from
60
+ return false if until_time && t >= until_time
61
+
62
+ true
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ module Registry
5
+ # Register metadata with ISO 19135 governance roles: the register
6
+ # owner, register manager and control body for this (sub)register.
7
+ # FERIN registers federate as siblings — no register is the universal
8
+ # envelope.
9
+ class Register < Lutaml::Model::Serializable
10
+ attribute :register_id, :string # FERIN register identifier
11
+ attribute :name, :string
12
+ attribute :register_owner, :string
13
+ attribute :register_manager, :string
14
+ attribute :control_body, :string
15
+ attribute :homepage, :string
16
+
17
+ json do
18
+ map "register_id", to: :register_id
19
+ map "name", to: :name, render_nil: false
20
+ map "register_owner", to: :register_owner, render_nil: false
21
+ map "register_manager", to: :register_manager, render_nil: false
22
+ map "control_body", to: :control_body, render_nil: false
23
+ map "homepage", to: :homepage, render_nil: false
24
+ end
25
+
26
+ yaml do
27
+ map "register_id", to: :register_id
28
+ map "name", to: :name, render_nil: false
29
+ map "register_owner", to: :register_owner, render_nil: false
30
+ map "register_manager", to: :register_manager, render_nil: false
31
+ map "control_body", to: :control_body, render_nil: false
32
+ map "homepage", to: :homepage, render_nil: false
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ module Registry
5
+ # Store interface — the seam between the registry client and a source
6
+ # of register content (local files for tests and offline mirrors; an
7
+ # HTTP adapter against a live FERIN register endpoint can implement
8
+ # the same three methods without touching the client).
9
+ class Store
10
+ # @return [Array<Item>] all items in the store
11
+ def items
12
+ raise NotImplementedError, "#{self.class} must implement #items"
13
+ end
14
+
15
+ # @return [Array<ApplicabilityBinding>] all applicability bindings
16
+ def bindings
17
+ raise NotImplementedError, "#{self.class} must implement #bindings"
18
+ end
19
+
20
+ # @return [Register, nil] register metadata when available
21
+ def register
22
+ raise NotImplementedError, "#{self.class} must implement #register"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ # FERIN / ISO 19135-style federated registry access (framework layer L3,
5
+ # seam S4/S5): every data point, profile, transform, crypto suite and
6
+ # unit is defined ONCE in a register — principal register plus
7
+ # federated subregisters with 19135 governance roles — and every item
8
+ # has a lifecycle (valid / superseded / retired) and versioned
9
+ # supersession chains. Applicability bindings carry effective windows
10
+ # with retroactivity flags, which is what makes "which profiles applied
11
+ # at T" answerable as a legal as-of query.
12
+ module Registry
13
+ autoload :Register, "unidpp/registry/register"
14
+ autoload :ItemVersion, "unidpp/registry/item_version"
15
+ autoload :Item, "unidpp/registry/item"
16
+ autoload :ApplicabilityBinding, "unidpp/registry/applicability"
17
+ autoload :Store, "unidpp/registry/store"
18
+ autoload :FileStore, "unidpp/registry/file_store"
19
+ autoload :Client, "unidpp/registry/client"
20
+ end
21
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ # Verification verdict (I9 / I13): graded, never boolean. A verdict
5
+ # states which verification *reading* it answers, the *freshness* class
6
+ # of the underlying state, and a coverage report (which data points the
7
+ # verdict covers and which are missing).
8
+ #
9
+ # The three readings (because law needs all three):
10
+ # evidentiary what could a diligent verifier know at T, given the
11
+ # then-current trust state (protects good-faith actors)
12
+ # current_state fraud voids ab initio — prevents laundering
13
+ # cryptographic signature/chain validity alone
14
+ #
15
+ # Freshness verdicts degrade explicitly, never silently:
16
+ # fresh within the profile's freshness window
17
+ # stale past the window; still served, marked
18
+ # offline_degraded served from Tier-A carrier / cache, as-of stamped
19
+ # unavailable no current state could be established
20
+ class Verdict < Lutaml::Model::Serializable
21
+ READINGS = %w[evidentiary current_state cryptographic].freeze
22
+ FRESHNESS = %w[fresh stale offline_degraded unavailable].freeze
23
+
24
+ attribute :reading, :string, values: READINGS
25
+ attribute :freshness, :string, values: FRESHNESS
26
+ attribute :rendered_at, "Lutaml::Model::Type::DateTime"
27
+ # Coverage report: data point URIs covered by this verdict and still
28
+ # missing for the profile that asked.
29
+ attribute :covered, :string, collection: true, default: -> { [] }
30
+ attribute :missing, :string, collection: true, default: -> { [] }
31
+ attribute :notes, :string, collection: true, default: -> { [] }
32
+
33
+ json do
34
+ map "reading", to: :reading
35
+ map "freshness", to: :freshness
36
+ map "rendered_at", to: :rendered_at, render_nil: false
37
+ map "covered", to: :covered, render_nil: false
38
+ map "missing", to: :missing, render_nil: false
39
+ map "notes", to: :notes, render_nil: false
40
+ end
41
+
42
+ yaml do
43
+ map "reading", to: :reading
44
+ map "freshness", to: :freshness
45
+ map "rendered_at", to: :rendered_at, render_nil: false
46
+ map "covered", to: :covered, render_nil: false
47
+ map "missing", to: :missing, render_nil: false
48
+ map "notes", to: :notes, render_nil: false
49
+ end
50
+
51
+ # Reading predicates (evidentiary? / current_state? / cryptographic?)
52
+ # are generated by lutaml-model from the reading value enumeration.
53
+
54
+ # I13: degradation is explicit — anything not fresh is degraded and
55
+ # must be marked, never silently passed.
56
+ def degraded?
57
+ freshness != "fresh"
58
+ end
59
+
60
+ def complete?
61
+ missing.empty?
62
+ end
63
+
64
+ # @return [Rational, nil] covered / (covered + missing), nil when empty
65
+ def coverage_ratio
66
+ total = covered.size + missing.size
67
+ return nil if total.zero?
68
+
69
+ Rational(covered.size, total)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unidpp
4
+ VERSION = "0.1.0"
5
+ end
data/lib/unidpp.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UniDPP — Ruby library for the international Digital Product Passport
4
+ # framework (the UniDPP design framework invariants I1–I14; see https://github.com/unidpp).
5
+ #
6
+ # Loading discipline: this file is the only require point. Every other
7
+ # constant is lazily loaded through autoload entries declared here (and,
8
+ # for the Unidpp::Registry namespace, in unidpp/registry.rb) — no
9
+ # require_relative anywhere inside lib/.
10
+ require "lutaml/model"
11
+
12
+ module Unidpp
13
+ class Error < StandardError; end
14
+
15
+ autoload :VERSION, "unidpp/version"
16
+ autoload :CanonicalJson, "unidpp/canonical_json"
17
+ autoload :AnnexB, "unidpp/annexb"
18
+ autoload :Gs1, "unidpp/gs1"
19
+ autoload :ProductIdentifier, "unidpp/identifier"
20
+ autoload :ProfileManifest, "unidpp/manifest"
21
+ autoload :ProfileAxes, "unidpp/manifest"
22
+ autoload :TriggerPredicate, "unidpp/manifest"
23
+ autoload :CryptoSuiteBinding, "unidpp/manifest"
24
+ autoload :PassportLink, "unidpp/link"
25
+ autoload :LinkInterval, "unidpp/link"
26
+ autoload :Event, "unidpp/event"
27
+ autoload :EventLog, "unidpp/event_log"
28
+ autoload :Verdict, "unidpp/verdict"
29
+ autoload :Quantity, "unidpp/quantity"
30
+ autoload :Registry, "unidpp/registry"
31
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unidpp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - UniDPP
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lutaml-model
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.13'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.13'
54
+ description: 'Ruby library for the UniDPP international Digital Product Passport framework:
55
+ scheme-agnostic product identifiers (GS1 Digital Link, ISO/IEC 15459, GB/T 33993-style,
56
+ URN), profile manifests (jurisdiction x sector x characteristic axes, trigger predicates,
57
+ capability classes S0-S3, crypto-suite bindings), typed relationship edges with
58
+ visibility classes, hash-chained append-only event logs with salted commitments,
59
+ graded-trust verdicts, quantities with registered unit URIs, and a federated-registry
60
+ client (FERIN/ISO 19135 items, statuses, supersession chains, applicability as-of
61
+ queries).'
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - lib/unidpp.rb
67
+ - lib/unidpp/annexb.rb
68
+ - lib/unidpp/canonical_json.rb
69
+ - lib/unidpp/event.rb
70
+ - lib/unidpp/event_log.rb
71
+ - lib/unidpp/gs1.rb
72
+ - lib/unidpp/identifier.rb
73
+ - lib/unidpp/link.rb
74
+ - lib/unidpp/manifest.rb
75
+ - lib/unidpp/quantity.rb
76
+ - lib/unidpp/registry.rb
77
+ - lib/unidpp/registry/applicability.rb
78
+ - lib/unidpp/registry/client.rb
79
+ - lib/unidpp/registry/file_store.rb
80
+ - lib/unidpp/registry/item.rb
81
+ - lib/unidpp/registry/item_version.rb
82
+ - lib/unidpp/registry/register.rb
83
+ - lib/unidpp/registry/store.rb
84
+ - lib/unidpp/verdict.rb
85
+ - lib/unidpp/version.rb
86
+ homepage: https://github.com/unidpp/unidpp-rb
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '3.0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubygems_version: 4.0.23
105
+ specification_version: 4
106
+ summary: UniDPP — Digital Product Passport model, event log and FERIN/ISO 19135 registry
107
+ client
108
+ test_files: []