dcc 0.2.0 → 0.2.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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +19 -0
  3. data/.github/workflows/release.yml +28 -0
  4. data/.gitignore +4 -0
  5. data/.rubocop_todo.yml +560 -10
  6. data/CHANGELOG.adoc +34 -1
  7. data/CONTRIBUTING.adoc +8 -1
  8. data/Gemfile +5 -7
  9. data/README.adoc +162 -7
  10. data/dcc.gemspec +8 -2
  11. data/docs/schema-versions.adoc +20 -7
  12. data/examples/build_minimal_dcc.rb +0 -0
  13. data/examples/convert_all_formats.rb +2 -1
  14. data/examples/parse_and_inspect.rb +0 -0
  15. data/examples/quantity_math_demo.rb +7 -7
  16. data/examples/validate_full.rb +1 -1
  17. data/lib/dcc/base/calibration_laboratory.rb +2 -1
  18. data/lib/dcc/base/condition.rb +5 -2
  19. data/lib/dcc/base/core_data.rb +8 -3
  20. data/lib/dcc/base/formula.rb +3 -3
  21. data/lib/dcc/base/identification.rb +6 -3
  22. data/lib/dcc/base/mathml.rb +33 -0
  23. data/lib/dcc/base/quantity.rb +2 -1
  24. data/lib/dcc/base/resp_person.rb +4 -2
  25. data/lib/dcc/base/text.rb +1 -1
  26. data/lib/dcc/base.rb +2 -1
  27. data/lib/dcc/builder/dsl.rb +1 -1
  28. data/lib/dcc/builder/session.rb +16 -14
  29. data/lib/dcc/builder.rb +3 -3
  30. data/lib/dcc/cli/cli.rb +36 -18
  31. data/lib/dcc/cli/formatters.rb +35 -7
  32. data/lib/dcc/cli.rb +1 -1
  33. data/lib/dcc/context_configuration.rb +2 -1
  34. data/lib/dcc/context_options.rb +4 -2
  35. data/lib/dcc/convert/csv.rb +27 -9
  36. data/lib/dcc/convert/html.rb +26 -110
  37. data/lib/dcc/convert/json.rb +10 -7
  38. data/lib/dcc/convert/liquid_filters.rb +58 -0
  39. data/lib/dcc/convert/result.rb +1 -1
  40. data/lib/dcc/convert/templates/report.liquid +250 -0
  41. data/lib/dcc/convert/yaml.rb +1 -1
  42. data/lib/dcc/convert.rb +1 -0
  43. data/lib/dcc/diff/change.rb +1 -1
  44. data/lib/dcc/diff/result.rb +8 -4
  45. data/lib/dcc/diff.rb +43 -11
  46. data/lib/dcc/error.rb +7 -0
  47. data/lib/dcc/extract/file.rb +3 -4
  48. data/lib/dcc/extract/ring.rb +1 -1
  49. data/lib/dcc/extract.rb +1 -1
  50. data/lib/dcc/i18n/text_lookup.rb +8 -4
  51. data/lib/dcc/i18n.rb +1 -1
  52. data/lib/dcc/inspect/report.rb +51 -14
  53. data/lib/dcc/inspect.rb +1 -1
  54. data/lib/dcc/migrate/route.rb +44 -0
  55. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  56. data/lib/dcc/migrate.rb +130 -26
  57. data/lib/dcc/plugin/base.rb +64 -0
  58. data/lib/dcc/plugin/registry.rb +7 -2
  59. data/lib/dcc/plugin.rb +6 -2
  60. data/lib/dcc/quantity_format/formatter.rb +16 -10
  61. data/lib/dcc/quantity_format.rb +1 -1
  62. data/lib/dcc/quantity_math/quantity.rb +1 -1
  63. data/lib/dcc/quantity_math/real.rb +6 -6
  64. data/lib/dcc/quantity_math.rb +1 -1
  65. data/lib/dcc/schema/version.rb +2 -2
  66. data/lib/dcc/server/app.html +282 -0
  67. data/lib/dcc/server/app.rb +171 -87
  68. data/lib/dcc/server/demo_cert.xml +244 -0
  69. data/lib/dcc/server/storage/entry.rb +1 -1
  70. data/lib/dcc/server/storage/memory.rb +3 -2
  71. data/lib/dcc/server/storage.rb +1 -1
  72. data/lib/dcc/server.rb +1 -1
  73. data/lib/dcc/si/base/complex.rb +1 -1
  74. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  75. data/lib/dcc/si/base/constant.rb +1 -1
  76. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  77. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  78. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  79. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  80. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  81. data/lib/dcc/si/base/hybrid.rb +3 -2
  82. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  83. data/lib/dcc/si/base/real.rb +13 -3
  84. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  85. data/lib/dcc/si/base/si_list.rb +3 -2
  86. data/lib/dcc/si/base/standard_mu.rb +1 -1
  87. data/lib/dcc/si/base.rb +5 -2
  88. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  89. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  90. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  91. data/lib/dcc/si/v1.rb +5 -2
  92. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  93. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  94. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  95. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  96. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  97. data/lib/dcc/si/v2.rb +8 -3
  98. data/lib/dcc/signature/result.rb +2 -2
  99. data/lib/dcc/signature/signer.rb +31 -18
  100. data/lib/dcc/signature/verifier.rb +23 -21
  101. data/lib/dcc/signature.rb +1 -1
  102. data/lib/dcc/transform/result.rb +1 -1
  103. data/lib/dcc/transform/xslt.rb +22 -12
  104. data/lib/dcc/transform.rb +5 -4
  105. data/lib/dcc/type/base64_binary.rb +5 -3
  106. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  107. data/lib/dcc/type/iso_country_code.rb +3 -2
  108. data/lib/dcc/type/iso_language_code.rb +3 -2
  109. data/lib/dcc/type/schema_version.rb +3 -2
  110. data/lib/dcc/type/si_unit.rb +3 -2
  111. data/lib/dcc/type_guards.rb +3 -3
  112. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  113. data/lib/dcc/v2/condition.rb +22 -1
  114. data/lib/dcc/v2/configuration.rb +1 -1
  115. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  116. data/lib/dcc/v2/identification.rb +14 -1
  117. data/lib/dcc/v2/mathml.rb +8 -0
  118. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  119. data/lib/dcc/v2/quantity.rb +4 -2
  120. data/lib/dcc/v2.rb +4 -2
  121. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v3/configuration.rb +1 -1
  123. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  124. data/lib/dcc/v3/mathml.rb +8 -0
  125. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  126. data/lib/dcc/v3/quantity.rb +4 -2
  127. data/lib/dcc/v3.rb +12 -2
  128. data/lib/dcc/validate/business_rules.rb +23 -9
  129. data/lib/dcc/validate/issue.rb +2 -1
  130. data/lib/dcc/validate/result.rb +6 -4
  131. data/lib/dcc/validate/schematron/profile.rb +73 -18
  132. data/lib/dcc/validate/schematron/rule.rb +13 -3
  133. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  134. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  135. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  136. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  137. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  138. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  139. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  140. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  141. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  142. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  143. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  144. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  145. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  146. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  147. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  148. data/lib/dcc/validate/schematron/rules.rb +28 -14
  149. data/lib/dcc/validate/schematron.rb +1 -1
  150. data/lib/dcc/validate/xsd.rb +9 -3
  151. data/lib/dcc/validate.rb +1 -1
  152. data/lib/dcc/version.rb +1 -1
  153. data/lib/dcc/versioned_parser.rb +1 -1
  154. data/lib/dcc.rb +55 -7
  155. data/sample_dcc.xml +140 -0
  156. data/web/index.html +12 -0
  157. data/web/package-lock.json +1599 -0
  158. data/web/package.json +22 -0
  159. data/web/src/App.vue +349 -0
  160. data/web/src/lib/builder.ts +171 -0
  161. data/web/src/lib/dcc.ts +338 -0
  162. data/web/src/lib/units.json +5581 -0
  163. data/web/src/main.ts +5 -0
  164. data/web/src/style.css +27 -0
  165. data/web/tsconfig.json +15 -0
  166. data/web/vite.config.ts +14 -0
  167. metadata +125 -9
@@ -25,32 +25,32 @@ module Dcc
25
25
 
26
26
  # Top-level DSL methods. Each opens a nested container that captures
27
27
  # subsequent setter calls via instance_eval.
28
- def administrative_data(**attrs, &block)
28
+ def administrative_data(**attrs, &)
29
29
  apply_attrs(@administrative_in_progress, attrs)
30
- instance_eval(&block) if block_given?
30
+ instance_eval(&) if block_given?
31
31
  @administrative_in_progress
32
32
  end
33
33
 
34
- def core_data(**attrs, &block)
34
+ def core_data(**attrs, &)
35
35
  cd = new_core_data
36
36
  apply_attrs(cd, attrs)
37
37
  @administrative_in_progress.core_data = cd
38
- instance_eval(&block) if block_given?
38
+ instance_eval(&) if block_given?
39
39
  cd
40
40
  end
41
41
 
42
- def items(**attrs, &block)
42
+ def items(**attrs, &)
43
43
  items_obj = new_items
44
44
  apply_attrs(items_obj, attrs)
45
45
  @administrative_in_progress.items = items_obj
46
- instance_eval(&block) if block_given?
46
+ instance_eval(&) if block_given?
47
47
  items_obj
48
48
  end
49
49
 
50
- def item(**attrs, &block)
50
+ def item(**attrs, &)
51
51
  it = new_item
52
52
  apply_attrs(it, attrs)
53
- instance_eval(&block) if block_given?
53
+ instance_eval(&) if block_given?
54
54
  (@items_in_progress ||= []) << it
55
55
  # Wire into the open items container if present.
56
56
  if @administrative_in_progress&.items
@@ -60,14 +60,14 @@ module Dcc
60
60
  it
61
61
  end
62
62
 
63
- def measurement_results(**_attrs, &block)
64
- instance_eval(&block) if block_given?
63
+ def measurement_results(**_attrs, &)
64
+ instance_eval(&) if block_given?
65
65
  end
66
66
 
67
- def measurement_result(**attrs, &block)
67
+ def measurement_result(**attrs, &)
68
68
  mr = new_measurement_result
69
69
  apply_attrs(mr, attrs)
70
- instance_eval(&block) if block_given?
70
+ instance_eval(&) if block_given?
71
71
  @results_in_progress << mr
72
72
  mr
73
73
  end
@@ -130,7 +130,9 @@ module Dcc
130
130
  def apply_attrs(target, attrs)
131
131
  attrs.each do |k, v|
132
132
  setter = :"#{k}="
133
- target.public_send(setter, v) if Dcc::TypeGuards.has_attribute?(target, setter)
133
+ target.public_send(setter, v) if Dcc::TypeGuards.has_attribute?(
134
+ target, setter
135
+ )
134
136
  end
135
137
  end
136
138
 
@@ -167,4 +169,4 @@ module Dcc
167
169
  end
168
170
  end
169
171
  end
170
- end
172
+ end
data/lib/dcc/builder.rb CHANGED
@@ -23,12 +23,12 @@ module Dcc
23
23
  # @param version [Integer, nil] major DCC version (default: 3).
24
24
  # @yield [Dcc::Builder::Session] builder session.
25
25
  # @return [Dcc::V2::DigitalCalibrationCertificate, Dcc::V3::DigitalCalibrationCertificate]
26
- def call(version: 3, &block)
26
+ def call(version: 3, &)
27
27
  ::Dcc.load_all!
28
28
  session = Session.new(version: version)
29
- session.instance_eval(&block) if block_given?
29
+ session.instance_eval(&) if block_given?
30
30
  session.build
31
31
  end
32
32
  end
33
33
  end
34
- end
34
+ end
data/lib/dcc/cli/cli.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "tty-table"
5
4
 
6
5
  # `Dcc::Cli::Cli` is the Thor-based command-line interface. Each subcommand
7
6
  # (validate, convert, extract, signature, transform, inspect, diff, issue)
@@ -27,7 +26,8 @@ module Dcc
27
26
  true
28
27
  end
29
28
 
30
- desc "validate SCHEMA FILE", "Validate a DCC XML against XSD, Schematron, or both"
29
+ desc "validate SCHEMA FILE",
30
+ "Validate a DCC XML against XSD, Schematron, or both"
31
31
  long_desc <<~DESC
32
32
  Validates the given DCC XML file.
33
33
 
@@ -37,20 +37,21 @@ module Dcc
37
37
  $ dcc validate all cert.xml
38
38
  DESC
39
39
  method_option :version, type: :string, default: "auto",
40
- desc: "Schema version (e.g. '3.3.0', 'auto')"
40
+ desc: "Schema version (e.g. '3.3.0', 'auto')"
41
41
  method_option :format, type: :string, default: "text",
42
- enum: %w[text json yaml]
42
+ enum: %w[text json yaml]
43
43
  def validate(schema, file)
44
44
  Cli.ensure_loaded!
45
45
  xml = File.read(file)
46
46
  version = options[:version]
47
47
  result =
48
48
  case schema
49
- when "xsd" then ::Dcc::Validate::Xsd.call(xml, version: version)
49
+ when "xsd" then ::Dcc::Validate::Xsd.call(xml,
50
+ version: version)
50
51
  when "schematron" then ::Dcc::Validate::Schematron.call(Dcc.parse(xml))
51
52
  when "all"
52
53
  ::Dcc::Validate::Xsd.call(xml, version: version)
53
- .merge(::Dcc::Validate::Schematron.call(Dcc.parse(xml)))
54
+ .merge(::Dcc::Validate::Schematron.call(Dcc.parse(xml)))
54
55
  else
55
56
  abort "Unknown validation target: #{schema} (expected xsd|schematron|all)"
56
57
  end
@@ -58,17 +59,18 @@ module Dcc
58
59
  exit(1) unless result.ok?
59
60
  end
60
61
 
61
- desc "convert FORMAT FILE", "Convert a DCC XML file to json / yaml / csv / html"
62
+ desc "convert FORMAT FILE",
63
+ "Convert a DCC XML file to json / yaml / csv / html"
62
64
  method_option :output, type: :string, banner: "PATH",
63
- desc: "Write output to a file instead of stdout"
65
+ desc: "Write output to a file instead of stdout"
64
66
  def convert(format, file)
65
67
  Cli.ensure_loaded!
66
68
  xml = File.read(file)
67
- dcc = Dcc.parse(xml)
68
69
  result =
69
70
  case format
70
- when "json" then ::Dcc::Convert::Json.call(dcc)
71
- when "yaml" then ::Dcc::Convert::Yaml.call(dcc)
71
+ when "json" then ::Dcc::Convert::Json.call(Dcc.parse(xml))
72
+ when "yaml" then ::Dcc::Convert::Yaml.call(Dcc.parse(xml))
73
+ when "html" then ::Dcc::Convert::Html.call(xml)
72
74
  else
73
75
  abort "Format not yet implemented: #{format}"
74
76
  end
@@ -80,19 +82,24 @@ module Dcc
80
82
  end
81
83
  end
82
84
 
83
- desc "extract TARGET FILE", "Extract embedded files or quantities from a DCC"
85
+ desc "extract TARGET FILE",
86
+ "Extract embedded files or quantities from a DCC"
84
87
  method_option :index, type: :numeric, desc: "Index of the file to extract"
85
88
  method_option :output, type: :string, banner: "PATH",
86
- desc: "Write extracted payload to PATH"
89
+ desc: "Write extracted payload to PATH"
87
90
  method_option :ring, type: :string,
88
- enum: %w[administrativeData measurementResults comment document]
91
+ enum: %w[administrativeData measurementResults comment document]
89
92
  def extract(target, file)
90
93
  Cli.ensure_loaded!
91
94
  dcc = Dcc.parse(File.read(file))
92
95
  case target
93
96
  when "files"
94
97
  files = ::Dcc::Extract::File.each(dcc)
95
- files = files.select { |f| f.ring.to_s == options[:ring] } if options[:ring]
98
+ if options[:ring]
99
+ files = files.select do |f|
100
+ f.ring.to_s == options[:ring]
101
+ end
102
+ end
96
103
  if options[:index]
97
104
  f = files[options[:index].to_i] || abort("No file at index #{options[:index]}")
98
105
  output_to(f.data, options[:output], f.file_name)
@@ -106,7 +113,7 @@ module Dcc
106
113
 
107
114
  desc "inspect FILE", "Show a human-readable summary of a DCC"
108
115
  method_option :format, type: :string, default: "text",
109
- enum: %w[text json yaml]
116
+ enum: %w[text json yaml]
110
117
  def inspect(file)
111
118
  Cli.ensure_loaded!
112
119
  dcc = Dcc.parse(File.read(file))
@@ -116,7 +123,7 @@ module Dcc
116
123
 
117
124
  desc "diff FILE1 FILE2", "Compare two DCC files structurally"
118
125
  method_option :format, type: :string, default: "text",
119
- enum: %w[text json]
126
+ enum: %w[text json]
120
127
  def diff(file1, file2)
121
128
  Cli.ensure_loaded!
122
129
  dcc1 = Dcc.parse(File.read(file1))
@@ -125,6 +132,17 @@ module Dcc
125
132
  ::Dcc::Cli::Formatters.print(diff, format: options[:format])
126
133
  end
127
134
 
135
+ desc "serve", "Start the DCC web validator (http://localhost:4567)"
136
+ method_option :port, type: :numeric, default: 4567, desc: "Port"
137
+ method_option :bind, type: :string, default: "0.0.0.0",
138
+ desc: "Bind address"
139
+ def serve
140
+ require "dcc/server"
141
+ ::Dcc.load_all!
142
+ puts "Starting DCC Validator on http://localhost:#{options[:port]}"
143
+ ::Dcc::Server::App.run!(port: options[:port], bind: options[:bind])
144
+ end
145
+
128
146
  desc "version", "Print the dcc gem version"
129
147
  def version
130
148
  puts "dcc #{::Dcc::VERSION}"
@@ -146,4 +164,4 @@ module Dcc
146
164
  end
147
165
  end
148
166
  end
149
- end
167
+ end
@@ -1,12 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-table"
4
-
5
3
  module Dcc
6
4
  module Cli
7
5
  # `Dcc::Cli::Formatters` picks the right text/JSON/YAML representation
8
6
  # of a result model and prints it.
9
7
  module Formatters
8
+ # Goes to stderr, so a piped `extract files` still yields clean data.
9
+ TTY_TABLE_HINT = "note: install tty-table for aligned output " \
10
+ "(gem install tty-table)"
11
+
12
+ # `tty-table` is optional. It pulls in eight further gems to prettify
13
+ # one command, which every library user would otherwise install just
14
+ # to parse XML. Loaded on first use, and its absence falls back to the
15
+ # plain renderer rather than raising the way `Dcc::Server` does.
16
+ def self.table_available?
17
+ return @table_available unless @table_available.nil?
18
+
19
+ begin
20
+ require "tty-table"
21
+ @table_available = true
22
+ rescue ::LoadError
23
+ @table_available = false
24
+ end
25
+ @table_available
26
+ end
27
+
10
28
  module_function
11
29
 
12
30
  def print(object, format: "text")
@@ -14,7 +32,7 @@ module Dcc
14
32
  when "json" then puts object.to_json
15
33
  when "yaml" then puts object.to_yaml
16
34
  else
17
- puts object.to_s
35
+ puts object
18
36
  end
19
37
  end
20
38
 
@@ -24,6 +42,14 @@ module Dcc
24
42
  return
25
43
  end
26
44
 
45
+ unless table_available?
46
+ # Hint first: a stderr that raises is caught by the rescue below,
47
+ # which re-runs the renderer. Printing first would duplicate it.
48
+ warn TTY_TABLE_HINT
49
+ print_files_plain(files)
50
+ return
51
+ end
52
+
27
53
  # TTY::Table calls ioctl which fails on StringIO / pipes. Fall back
28
54
  # to a plain-text rendering when stdout isn't a real terminal.
29
55
  if $stdout.is_a?(::IO) && !$stdout.tty?
@@ -34,7 +60,8 @@ module Dcc
34
60
  table = ::TTY::Table.new(
35
61
  ["Index", "Name", "File Name", "MIME", "Ring", "Size"],
36
62
  files.each_with_index.map do |f, i|
37
- [i, f.name.to_s, f.file_name.to_s, f.mime_type.to_s, f.ring, f.data.bytesize]
63
+ [i, f.name.to_s, f.file_name.to_s, f.mime_type.to_s, f.ring,
64
+ f.data.bytesize]
38
65
  end,
39
66
  )
40
67
  puts table.render
@@ -45,8 +72,9 @@ module Dcc
45
72
 
46
73
  def print_files_plain(files)
47
74
  # Widths
48
- widths = ["Index".size, "Name".size, "File Name".size, "MIME".size, "Ring".size, "Size".size]
49
- rows = files.each_with_index.map do |f, i|
75
+ widths = ["Index".size, "Name".size, "File Name".size, "MIME".size,
76
+ "Ring".size, "Size".size]
77
+ files.each_with_index.map do |f, i|
50
78
  [
51
79
  i.to_s,
52
80
  f.name.to_s,
@@ -74,4 +102,4 @@ module Dcc
74
102
  end
75
103
  end
76
104
  end
77
- end
105
+ end
data/lib/dcc/cli.rb CHANGED
@@ -4,4 +4,4 @@
4
4
  module Dcc
5
5
  module Cli
6
6
  end
7
- end
7
+ end
@@ -34,7 +34,8 @@ module Dcc
34
34
  # @param registry [Lutaml::Model::TypeRegistry, nil] custom registry.
35
35
  # @param fallback_to [Array<Symbol>] fallback chain for type lookup.
36
36
  # @param substitutions [Array<Hash{from_type:, to_type:}>] type substitutions.
37
- def create_context(id:, registry: nil, fallback_to: [context_id], substitutions: [])
37
+ def create_context(id:, registry: nil, fallback_to: [context_id],
38
+ substitutions: [])
38
39
  normalized_id = ::Dcc::ContextOptions.normalize_context_reference(id)
39
40
  return populate_context! if normalized_id == context_id
40
41
 
@@ -13,7 +13,8 @@ module Dcc
13
13
  # @param register [Symbol, String, nil] deprecated alias for `context:`.
14
14
  # @param default_context [Symbol, nil] returned when neither is given.
15
15
  # @param warning_source [String] caller name for deprecation warnings.
16
- def normalize_context_option(context:, register:, default_context:, warning_source:)
16
+ def normalize_context_option(context:, register:, default_context:,
17
+ warning_source:)
17
18
  raise_if_conflicting!(context, register)
18
19
  return normalize_context_reference(register) || default_context if register
19
20
  return default_context unless context_specified?(context)
@@ -29,7 +30,8 @@ module Dcc
29
30
  return reference.to_sym if reference.is_a?(String)
30
31
  return reference.id.to_sym if reference.is_a?(::Lutaml::Xml::Namespace)
31
32
 
32
- raise ArgumentError, "Unsupported context/register reference: #{reference.inspect}"
33
+ raise ArgumentError,
34
+ "Unsupported context/register reference: #{reference.inspect}"
33
35
  end
34
36
 
35
37
  def context_specified?(context)
@@ -7,7 +7,8 @@ module Dcc
7
7
  # `Dcc::Convert::Csv` flattens a parsed DCC into a tabular CSV with
8
8
  # one row per measurement quantity.
9
9
  module Csv
10
- HEADERS = %w[result_name quantity_name quantity_type value unit uncertainty coverage_factor].freeze
10
+ HEADERS = %w[result_name quantity_name quantity_type value unit
11
+ uncertainty coverage_factor].freeze
11
12
 
12
13
  class << self
13
14
  # @param dcc [Lutaml::Model::Serializable]
@@ -31,7 +32,8 @@ module Dcc
31
32
 
32
33
  def collect_quantities(dcc)
33
34
  rows = []
34
- return rows unless Dcc::TypeGuards.has_attribute?(dcc, :measurement_results)
35
+ return rows unless Dcc::TypeGuards.has_attribute?(dcc,
36
+ :measurement_results)
35
37
 
36
38
  Array(dcc.measurement_results.measurement_result).each do |mr|
37
39
  mr_name = name_of(mr)
@@ -53,7 +55,9 @@ module Dcc
53
55
  walk_quantities(Array(data.quantity), mr_name, result_name, rows)
54
56
  Array(data.list).each do |list|
55
57
  walk_quantities(Array(list.quantity), mr_name, result_name, rows)
56
- Array(list.list).each { |sub| walk_data(sub, mr_name, result_name, rows) }
58
+ Array(list.list).each do |sub|
59
+ walk_data(sub, mr_name, result_name, rows)
60
+ end
57
61
  end
58
62
  end
59
63
 
@@ -63,10 +67,10 @@ module Dcc
63
67
  end
64
68
  end
65
69
 
66
- def push_quantity(q, mr_name, result_name, kind, rows)
70
+ def push_quantity(q, mr_name, _result_name, kind, rows)
67
71
  rows << [
68
72
  mr_name, name_of(q), kind,
69
- value_of(q), unit_of(q), uncertainty_of(q), k_factor_of(q),
73
+ value_of(q), unit_of(q), uncertainty_of(q), k_factor_of(q)
70
74
  ]
71
75
  end
72
76
 
@@ -74,11 +78,21 @@ module Dcc
74
78
  return "" unless node && Dcc::TypeGuards.has_attribute?(node, :name)
75
79
  return "" unless node.name
76
80
 
77
- content = Dcc::TypeGuards.has_attribute?(node.name, :content) ? node.name.content : []
81
+ content = if Dcc::TypeGuards.has_attribute?(node.name,
82
+ :content)
83
+ node.name.content
84
+ else
85
+ []
86
+ end
78
87
  first = Array(content).first
79
88
  return "" unless first
80
89
 
81
- vals = Dcc::TypeGuards.has_attribute?(first, :value) ? first.value : []
90
+ vals = if Dcc::TypeGuards.has_attribute?(first,
91
+ :value)
92
+ first.value
93
+ else
94
+ []
95
+ end
82
96
  Array(vals).first.to_s
83
97
  end
84
98
 
@@ -87,7 +101,11 @@ module Dcc
87
101
  return reals.first.value.to_s if reals.any?
88
102
 
89
103
  lists = Array(q.real_list_xml_list)
90
- return lists.first.value_xml_list.map(&:to_s).join("; ") if lists.any?
104
+ # Serialized rather than mapped over: BigDecimal#to_s renders 0.072
105
+ # as "0.72e-1", which is not what the document said.
106
+ if lists.any?
107
+ return ::Dcc::Type::DecimalXmlList.serialize(lists.first.value_xml_list)
108
+ end
91
109
 
92
110
  ""
93
111
  end
@@ -126,4 +144,4 @@ module Dcc
126
144
  end
127
145
  end
128
146
  end
129
- end
147
+ end
@@ -1,127 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "liquid"
4
+
3
5
  module Dcc
4
6
  module Convert
5
- # `Dcc::Convert::Html` renders a parsed DCC into a human-readable HTML
6
- # report with collapsible sections per administrative section and
7
- # measurement result.
8
7
  module Html
9
- class << self
10
- # @param dcc [Lutaml::Model::Serializable]
11
- # @return [Dcc::Convert::Result]
12
- def call(dcc)
13
- payload = <<~HTML
14
- <!DOCTYPE html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="utf-8">
18
- <title>DCC Report #{escape(unique_identifier_of(dcc))}</title>
19
- <style>
20
- body { font-family: -apple-system, system-ui, sans-serif; margin: 2rem; }
21
- h1, h2, h3 { color: #333; }
22
- section { margin-bottom: 1.5rem; padding: 1rem; border: 1px solid #eee; border-radius: 4px; }
23
- table { border-collapse: collapse; }
24
- th, td { border: 1px solid #ddd; padding: 0.5rem 1rem; text-align: left; }
25
- th { background: #f5f5f5; }
26
- </style>
27
- </head>
28
- <body>
29
- <h1>Digital Calibration Certificate</h1>
30
- #{summary_table(dcc)}
31
- #{administrative_section(dcc)}
32
- #{measurement_section(dcc)}
33
- </body>
34
- </html>
35
- HTML
8
+ TEMPLATE_PATH = File.expand_path("templates/report.liquid",
9
+ __dir__).freeze
36
10
 
11
+ class << self
12
+ def call(dcc_or_xml, lang: nil)
13
+ if dcc_or_xml.is_a?(::String)
14
+ xml = dcc_or_xml
15
+ hash = ::Dcc::Convert::Json.xml_to_hash(xml)
16
+ schema_version = hash["schemaVersion"]
17
+ source_class = "xml"
18
+ else
19
+ xml = dcc_or_xml.to_xml
20
+ hash = ::Dcc::Convert::Json.xml_to_hash(xml)
21
+ schema_version = dcc_or_xml.schema_version.to_s
22
+ source_class = dcc_or_xml.class.name
23
+ end
24
+ rendered = render_template(hash, lang: lang)
37
25
  ::Dcc::Convert::Result.new(
38
26
  format: :html,
39
- payload: payload,
40
- source_class: dcc.class.name,
41
- schema_version: dcc.schema_version.to_s,
27
+ payload: rendered,
28
+ source_class: source_class,
29
+ schema_version: schema_version,
42
30
  )
43
31
  end
44
32
 
45
33
  private
46
34
 
47
- def summary_table(dcc)
48
- <<~HTML
49
- <section>
50
- <h2>Identity</h2>
51
- <table>
52
- <tr><th>Schema version</th><td>#{escape(dcc.schema_version)}</td></tr>
53
- <tr><th>Unique identifier</th><td>#{escape(unique_identifier_of(dcc))}</td></tr>
54
- <tr><th>Country</th><td>#{escape(country_of(dcc))}</td></tr>
55
- </table>
56
- </section>
57
- HTML
58
- end
59
-
60
- def administrative_section(dcc)
61
- return "" unless Dcc::TypeGuards.has_attribute?(dcc, :administrative_data) && dcc.administrative_data
62
-
63
- admin = dcc.administrative_data
64
- items = Dcc::TypeGuards.has_attribute?(admin, :items) && admin.items ? Array(admin.items.item) : []
65
- <<~HTML
66
- <section>
67
- <h2>Items (#{items.size})</h2>
68
- <ul>
69
- #{items.map { |i| "<li>#{escape(name_of(i))}</li>" }.join("\n")}
70
- </ul>
71
- </section>
72
- HTML
73
- end
74
-
75
- def measurement_section(dcc)
76
- return "" unless Dcc::TypeGuards.has_attribute?(dcc, :measurement_results) && dcc.measurement_results
77
-
78
- mrs = Array(dcc.measurement_results.measurement_result)
79
- <<~HTML
80
- <section>
81
- <h2>Measurement Results (#{mrs.size})</h2>
82
- <ul>
83
- #{mrs.map { |mr| "<li>#{escape(name_of(mr))}</li>" }.join("\n")}
84
- </ul>
85
- </section>
86
- HTML
87
- end
88
-
89
- def unique_identifier_of(dcc)
90
- Dcc::TypeGuards.has_attribute?(dcc, :administrative_data) &&
91
- Dcc::TypeGuards.has_attribute?(dcc.administrative_data, :core_data) &&
92
- dcc.administrative_data.core_data &&
93
- dcc.administrative_data.core_data.unique_identifier.to_s
94
- end
95
-
96
- def country_of(dcc)
97
- Dcc::TypeGuards.has_attribute?(dcc, :administrative_data) &&
98
- Dcc::TypeGuards.has_attribute?(dcc.administrative_data, :core_data) &&
99
- dcc.administrative_data.core_data &&
100
- dcc.administrative_data.core_data.country_code_iso_3166_1.to_s
101
- end
102
-
103
- def name_of(node)
104
- return "" unless node && Dcc::TypeGuards.has_attribute?(node, :name)
105
- return "" unless node.name
106
-
107
- content = Dcc::TypeGuards.has_attribute?(node.name, :content) ? node.name.content : []
108
- first = Array(content).first
109
- return "" unless first
110
-
111
- vals = Dcc::TypeGuards.has_attribute?(first, :value) ? first.value : []
112
- Array(vals).first.to_s
113
- end
114
-
115
- def escape(s)
116
- return "" if s.nil?
117
-
118
- s.to_s
119
- .gsub("&", "&amp;")
120
- .gsub("<", "&lt;")
121
- .gsub(">", "&gt;")
122
- .gsub('"', "&quot;")
35
+ def render_template(hash, lang:)
36
+ template = ::Liquid::Template.parse(File.read(TEMPLATE_PATH))
37
+ template.render({ "dcc" => hash, "page_lang" => lang },
38
+ filters: [::Dcc::Convert::LiquidFilters])
123
39
  end
124
40
  end
125
41
  end
126
42
  end
127
- end
43
+ end
@@ -45,18 +45,21 @@ module Dcc
45
45
  return hash
46
46
  end
47
47
 
48
+ grouped = {}
48
49
  children.each do |child|
49
50
  name = child.name.to_s.split(":").last
50
- value = element_to_hash(child)
51
- if hash.key?(name)
52
- hash[name] = Array(hash[name]) << value
53
- else
54
- hash[name] = value
55
- end
51
+ grouped[name] ||= []
52
+ grouped[name] << child
56
53
  end
54
+
55
+ grouped.each do |name, child_list|
56
+ values = child_list.map { |c| element_to_hash(c) }
57
+ hash[name] = values.size == 1 ? values.first : values
58
+ end
59
+ hash["_text"] = combined_text unless combined_text.empty?
57
60
  hash
58
61
  end
59
62
  end
60
63
  end
61
64
  end
62
- end
65
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dcc
4
+ module Convert
5
+ module LiquidFilters
6
+ def dcc_text(content, lang = nil)
7
+ return "" if content.nil? || content == ""
8
+
9
+ case content
10
+ when ::String
11
+ content
12
+ when ::Hash
13
+ content["_text"] || content[:_text] || content.to_s
14
+ when ::Array
15
+ if lang
16
+ entry = content.find do |c|
17
+ c.is_a?(::Hash) && (c["lang"] == lang || c[:lang] == lang)
18
+ end
19
+ return dcc_text(entry, lang) if entry
20
+ end
21
+ first = content.find { |c| c.is_a?(::Hash) }
22
+ first ? (first["_text"] || first[:_text] || "") : ""
23
+ else
24
+ content.to_s
25
+ end
26
+ end
27
+
28
+ def dcc_initials(name)
29
+ return "?" if name.nil? || name.empty?
30
+
31
+ parts = name.split
32
+ return parts.first[0..0].upcase if parts.size == 1
33
+
34
+ (parts.first[0] + parts.last[0]).upcase
35
+ end
36
+
37
+ def dcc_count(obj)
38
+ return 0 if obj.nil?
39
+
40
+ case obj
41
+ when ::Array then obj.size
42
+ when ::Hash then 1
43
+ else 1
44
+ end
45
+ end
46
+
47
+ def dcc_items(obj)
48
+ return [] if obj.nil?
49
+
50
+ case obj
51
+ when ::Array then obj
52
+ when ::Hash then [obj]
53
+ else [obj]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end