Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ RSpec/MultipleExpectations:
- 'spec/dcc/extract/file_spec.rb'
- 'spec/dcc/i18n/text_lookup_spec.rb'
- 'spec/dcc/inspect/report_spec.rb'
- 'spec/dcc/migrate_spec.rb'
- 'spec/dcc/namespace_spec.rb'
- 'spec/dcc/plugin_spec.rb'
- 'spec/dcc/quantity_format/formatter_spec.rb'
Expand All @@ -398,6 +399,7 @@ RSpec/ExampleLength:
- 'spec/dcc/cli/formatters_spec.rb'
- 'spec/dcc/convert/additional_spec.rb'
- 'spec/dcc/extract/file_spec.rb'
- 'spec/dcc/migrate_spec.rb'
- 'spec/dcc/type_spec.rb'
- 'spec/dcc/validate/business_rules_spec.rb'
- 'spec/dcc/validate_spec.rb'
Expand Down
7 changes: 5 additions & 2 deletions lib/dcc/base/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
module Dcc
module Base
# `dcc:conditionType` — single influence condition: name + description +
# optional state enum (beforeAdjustment, etc.) + data.
# optional status enum (beforeAdjustment, etc.) + data.
#
# v3 calls that child `status`; v2 calls it `state`, which
# `Dcc::V2::Condition` maps.
module Condition
def self.included(klass)
klass.class_eval do
Expand All @@ -24,7 +27,7 @@ def self.included(klass)
map_attribute "refType", to: :ref_type
map_element "name", to: :name
map_element "description", to: :description
map_element "state", to: :status
map_element "status", to: :status
map_element "data", to: :data
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/dcc/base/core_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def self.included(klass)
attribute :receipt_date, :date
attribute :begin_performance_date, :date
attribute :end_performance_date, :date
attribute :performance_location, :string
attribute :previous_report, :previousReport

xml do
Expand All @@ -38,6 +39,7 @@ def self.included(klass)
map_element "receiptDate", to: :receipt_date
map_element "beginPerformanceDate", to: :begin_performance_date
map_element "endPerformanceDate", to: :end_performance_date
map_element "performanceLocation", to: :performance_location
map_element "previousReport", to: :previous_report
end
end
Expand Down
9 changes: 6 additions & 3 deletions lib/dcc/base/identification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
module Dcc
module Base
# `dcc:identificationType` — single identifier (issuer, value, optional
# description). Issuer enum: manufacturer, calibrationLaboratory, customer,
# name). Issuer enum: manufacturer, calibrationLaboratory, customer,
# owner, other.
#
# v3 calls the third child `name`; v2 calls it `description`, which
# `Dcc::V2::Identification` maps.
module Identification
def self.included(klass)
klass.class_eval do
attribute :issuer, :string
attribute :value, :string
attribute :description, :text
attribute :name, :text

xml do
namespace ::Dcc::Namespace::Dcc
element "identification"
ordered
map_element "issuer", to: :issuer
map_element "value", to: :value
map_element "description", to: :description
map_element "name", to: :name
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/dcc/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class TransformError < Error; end
# Raised when an unsupported schema version is requested.
class UnknownVersionError < Error; end

# Raised when a migration direction is not supported — any downgrade, and
# any pair whose incompatibilities have not been verified.
class UnsupportedMigrationError < Error; end

# Raised when the builder DSL is used incorrectly.
class BuilderError < Error; end

Expand Down
85 changes: 60 additions & 25 deletions lib/dcc/migrate.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,75 @@
# frozen_string_literal: true

# `Dcc::Migrate` upgrades or downgrades a parsed DCC between schema versions,
# applying the necessary field renames and D-SI version transitions.
# `Dcc::Migrate` upgrades a parsed DCC to a newer schema version.
#
# Currently a thin shim: serializes the source DCC and re-parses under
# the target version's context. Future work will add per-field transformations.
# Migration is a document transform: the source is serialised, the route's
# field rules are applied to the XML, and the result is parsed under the
# target version. The input object is never modified.
#
# Migrations are one-way. A downgrade cannot preserve the source — v3 adds
# elements and attributes that no older schema accepts — so downgrading warns
# and then raises rather than returning a quietly invalid document.
module Dcc
module Migrate
autoload :Route, "dcc/migrate/route"
autoload :V2ToV3, "dcc/migrate/v2_to_v3"

class << self
# @param dcc [Lutaml::Model::Serializable]
# @param from [String] source version (e.g. "2.3.0").
# @param to [String] target version (e.g. "3.3.0").
# @return [Dcc::V2::DigitalCalibrationCertificate, Dcc::V3::DigitalCalibrationCertificate]
# @param dcc [Lutaml::Model::Serializable] parsed DCC.
# @param from [String] source version, e.g. "2.3.0".
# @param to [String] target version, e.g. "3.3.0".
# @raise [Dcc::UnknownVersionError] if either version is not bundled.
# @raise [Dcc::UnsupportedMigrationError] on a downgrade, or on a pair
# whose incompatibilities have not been verified.
# @return [Dcc::V2::DigitalCalibrationCertificate,
# Dcc::V3::DigitalCalibrationCertificate]
def call(dcc, from:, to:)
from_major = ::Dcc::Schema::Version.major(from)
to_major = ::Dcc::Schema::Version.major(to)
if from_major == to_major
# Same major: just rewrite the schemaVersion attribute.
return rewrite_schema_version(dcc, to)
end

# Cross-major migration: serialize then re-parse under target.
xml = dcc.to_xml
target_parser = ::Dcc.parser_for(to_major)
target_parser.load_all!
migrated = target_parser.parse(xml)
rewrite_schema_version(migrated, to)
reject_unparsed(dcc)
source = ::Dcc::Schema::Version.resolve_dcc(from)
target = ::Dcc::Schema::Version.resolve_dcc(to)
return dcc if source == target

reject_downgrade(source, target)
reject_unregistered(source, target)

transform = Route.for(source, target)
xml = transform ? transform.call(dcc.to_xml, to: target) : dcc.to_xml
parse_as(xml, target)
end

private

def rewrite_schema_version(dcc, version)
return dcc unless Dcc::TypeGuards.has_writer?(dcc, :schema_version)
def reject_unparsed(dcc)
return if dcc.is_a?(::Lutaml::Model::Serializable)

raise ::Dcc::Error,
"Dcc.migrate expects a parsed DCC document, got " \
"#{dcc.class}. Parse the XML with Dcc.parse first."
end

def reject_downgrade(source, target)
return if ::Gem::Version.new(target) > ::Gem::Version.new(source)

message = "Cannot migrate #{source} to #{target}: downgrading " \
"discards information the older schema cannot represent."
Kernel.warn(message)
raise ::Dcc::UnsupportedMigrationError, message
end

def reject_unregistered(source, target)
return if Route.supported?(source, target)

raise ::Dcc::UnsupportedMigrationError,
"No verified migration from #{source} to #{target}. " \
"Supported: #{Route.supported_pairs.join(', ')}."
end

dcc.schema_version = version
dcc
def parse_as(xml, version)
parser = ::Dcc.parser_for(::Dcc::Schema::Version.major(version))
parser.load_all!
parsed = parser.parse(xml)
parsed.schema_version = version
parsed
end
end
end
Expand Down
43 changes: 43 additions & 0 deletions lib/dcc/migrate/route.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module Dcc
module Migrate
# Registry of migrations whose full incompatibility set has been verified
# against the bundled XSDs.
#
# Unregistered pairs raise rather than run. DCC 2.1.0 and 2.1.1 sit in the
# `https://ptb.de/si/smartcom/d-si/v1_0_1` D-SI namespace, which no
# transform here addresses, so accepting them by major version alone would
# silently emit an unmigrated document.
module Route
TRANSFORMS = { %w[2.3.0 3.3.0] => :V2ToV3 }.freeze

# v3.3.0 is a strict superset of v3.2.1 at instance level; only the
# `schemaVersion` pattern differs.
SCHEMA_VERSION_ONLY = [%w[3.2.1 3.3.0]].freeze

class << self
# @param from [String] normalized source version, e.g. "2.3.0".
# @param to [String] normalized target version, e.g. "3.3.0".
# @return [Boolean] whether this migration is registered.
def supported?(from, to)
TRANSFORMS.key?([from, to]) ||
SCHEMA_VERSION_ONLY.include?([from, to])
end

# @return [Module, nil] a module responding to `.call(xml, to:)`, or
# nil when only `schemaVersion` needs rewriting.
def for(from, to)
name = TRANSFORMS[[from, to]]
name && ::Dcc::Migrate.const_get(name)
end

# @return [Array<String>] every registered pair, for error messages.
def supported_pairs
(TRANSFORMS.keys + SCHEMA_VERSION_ONLY)
.map { |from, to| "#{from} to #{to}" }
end
end
end
end
end
Loading
Loading