dcc 0.2.0 → 0.2.2

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 (176) 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 +9 -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 +152 -26
  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/formula/ast.rb +137 -0
  49. data/lib/dcc/extract/formula/evaluator.rb +357 -0
  50. data/lib/dcc/extract/formula/parser.rb +305 -0
  51. data/lib/dcc/extract/formula/quantity.rb +89 -0
  52. data/lib/dcc/extract/formula.rb +110 -0
  53. data/lib/dcc/extract/ring.rb +1 -1
  54. data/lib/dcc/extract.rb +1 -1
  55. data/lib/dcc/i18n/text_lookup.rb +8 -4
  56. data/lib/dcc/i18n.rb +1 -1
  57. data/lib/dcc/inspect/report.rb +51 -14
  58. data/lib/dcc/inspect.rb +1 -1
  59. data/lib/dcc/migrate/route.rb +44 -0
  60. data/lib/dcc/migrate/v2_to_v3.rb +445 -0
  61. data/lib/dcc/migrate.rb +130 -26
  62. data/lib/dcc/plugin/base.rb +64 -0
  63. data/lib/dcc/plugin/registry.rb +7 -2
  64. data/lib/dcc/plugin.rb +6 -2
  65. data/lib/dcc/quantity_format/formatter.rb +16 -10
  66. data/lib/dcc/quantity_format.rb +1 -1
  67. data/lib/dcc/quantity_math/complex.rb +252 -0
  68. data/lib/dcc/quantity_math/matrix.rb +217 -0
  69. data/lib/dcc/quantity_math/quantity.rb +1 -1
  70. data/lib/dcc/quantity_math/real.rb +6 -6
  71. data/lib/dcc/quantity_math.rb +31 -3
  72. data/lib/dcc/schema/version.rb +2 -2
  73. data/lib/dcc/server/app.html +282 -0
  74. data/lib/dcc/server/app.rb +171 -87
  75. data/lib/dcc/server/demo_cert.xml +244 -0
  76. data/lib/dcc/server/storage/entry.rb +1 -1
  77. data/lib/dcc/server/storage/memory.rb +3 -2
  78. data/lib/dcc/server/storage.rb +1 -1
  79. data/lib/dcc/server.rb +1 -1
  80. data/lib/dcc/si/base/complex.rb +1 -1
  81. data/lib/dcc/si/base/complex_list_xml_list.rb +1 -1
  82. data/lib/dcc/si/base/constant.rb +1 -1
  83. data/lib/dcc/si/base/coverage_interval.rb +36 -0
  84. data/lib/dcc/si/base/coverage_interval_mu.rb +1 -1
  85. data/lib/dcc/si/base/expanded_mu.rb +1 -1
  86. data/lib/dcc/si/base/expanded_unc.rb +34 -0
  87. data/lib/dcc/si/base/expanded_unc_xml_list.rb +3 -2
  88. data/lib/dcc/si/base/hybrid.rb +3 -2
  89. data/lib/dcc/si/base/measurement_uncertainty_univariate.rb +1 -1
  90. data/lib/dcc/si/base/real.rb +13 -3
  91. data/lib/dcc/si/base/real_list_xml_list.rb +5 -3
  92. data/lib/dcc/si/base/si_list.rb +3 -2
  93. data/lib/dcc/si/base/standard_mu.rb +1 -1
  94. data/lib/dcc/si/base.rb +5 -2
  95. data/lib/dcc/si/v1/coverage_interval.rb +8 -0
  96. data/lib/dcc/si/v1/expanded_unc.rb +1 -16
  97. data/lib/dcc/si/v1/measurement_uncertainty_univariate.rb +2 -1
  98. data/lib/dcc/si/v1.rb +5 -2
  99. data/lib/dcc/si/v2/coverage_interval.rb +8 -0
  100. data/lib/dcc/si/v2/coverage_interval_mu_xml_list.rb +4 -2
  101. data/lib/dcc/si/v2/expanded_mu_xml_list.rb +2 -1
  102. data/lib/dcc/si/v2/expanded_unc.rb +8 -0
  103. data/lib/dcc/si/v2/measurement_uncertainty_univariate.rb +2 -1
  104. data/lib/dcc/si/v2.rb +8 -3
  105. data/lib/dcc/signature/result.rb +2 -2
  106. data/lib/dcc/signature/signer.rb +31 -18
  107. data/lib/dcc/signature/verifier.rb +23 -21
  108. data/lib/dcc/signature.rb +1 -1
  109. data/lib/dcc/streaming/reader.rb +246 -0
  110. data/lib/dcc/streaming.rb +79 -0
  111. data/lib/dcc/transform/result.rb +1 -1
  112. data/lib/dcc/transform/xslt.rb +22 -12
  113. data/lib/dcc/transform.rb +5 -4
  114. data/lib/dcc/type/base64_binary.rb +5 -3
  115. data/lib/dcc/type/decimal_xml_list.rb +99 -13
  116. data/lib/dcc/type/iso_country_code.rb +3 -2
  117. data/lib/dcc/type/iso_language_code.rb +3 -2
  118. data/lib/dcc/type/schema_version.rb +3 -2
  119. data/lib/dcc/type/si_unit.rb +3 -2
  120. data/lib/dcc/type_guards.rb +3 -3
  121. data/lib/dcc/v2/calibration_laboratory.rb +2 -1
  122. data/lib/dcc/v2/condition.rb +22 -1
  123. data/lib/dcc/v2/configuration.rb +1 -1
  124. data/lib/dcc/v2/digital_calibration_certificate.rb +2 -1
  125. data/lib/dcc/v2/identification.rb +14 -1
  126. data/lib/dcc/v2/mathml.rb +8 -0
  127. data/lib/dcc/v2/measurement_meta_data_list.rb +2 -1
  128. data/lib/dcc/v2/quantity.rb +4 -2
  129. data/lib/dcc/v2.rb +4 -2
  130. data/lib/dcc/v3/calibration_laboratory.rb +2 -1
  131. data/lib/dcc/v3/configuration.rb +1 -1
  132. data/lib/dcc/v3/digital_calibration_certificate.rb +2 -1
  133. data/lib/dcc/v3/mathml.rb +8 -0
  134. data/lib/dcc/v3/measurement_meta_data_list.rb +2 -1
  135. data/lib/dcc/v3/quantity.rb +4 -2
  136. data/lib/dcc/v3.rb +12 -2
  137. data/lib/dcc/validate/business_rules.rb +23 -9
  138. data/lib/dcc/validate/issue.rb +2 -1
  139. data/lib/dcc/validate/result.rb +6 -4
  140. data/lib/dcc/validate/schematron/profile.rb +73 -18
  141. data/lib/dcc/validate/schematron/rule.rb +13 -3
  142. data/lib/dcc/validate/schematron/rules/administrative_data_completeness.rb +11 -4
  143. data/lib/dcc/validate/schematron/rules/base.rb +1 -2
  144. data/lib/dcc/validate/schematron/rules/date_range_check.rb +11 -5
  145. data/lib/dcc/validate/schematron/rules/id_refid_linking.rb +0 -3
  146. data/lib/dcc/validate/schematron/rules/influence_conditions_placement.rb +22 -12
  147. data/lib/dcc/validate/schematron/rules/iso_code_validation.rb +31 -14
  148. data/lib/dcc/validate/schematron/rules/language_code_dedup.rb +12 -5
  149. data/lib/dcc/validate/schematron/rules/non_si_declaration.rb +7 -4
  150. data/lib/dcc/validate/schematron/rules/release_format_check.rb +7 -4
  151. data/lib/dcc/validate/schematron/rules/schema_version_check.rb +13 -9
  152. data/lib/dcc/validate/schematron/rules/uncertainty_consistency.rb +39 -10
  153. data/lib/dcc/validate/schematron/rules/unit_format_check.rb +2 -3
  154. data/lib/dcc/validate/schematron/rules/used_methods_placement.rb +15 -10
  155. data/lib/dcc/validate/schematron/rules/used_software_placement.rb +15 -10
  156. data/lib/dcc/validate/schematron/rules/xml_list_spacing.rb +4 -3
  157. data/lib/dcc/validate/schematron/rules.rb +28 -14
  158. data/lib/dcc/validate/schematron.rb +1 -1
  159. data/lib/dcc/validate/xsd.rb +9 -3
  160. data/lib/dcc/validate.rb +1 -1
  161. data/lib/dcc/version.rb +1 -1
  162. data/lib/dcc/versioned_parser.rb +1 -1
  163. data/lib/dcc.rb +68 -10
  164. data/sample_dcc.xml +140 -0
  165. data/web/index.html +12 -0
  166. data/web/package-lock.json +1599 -0
  167. data/web/package.json +22 -0
  168. data/web/src/App.vue +349 -0
  169. data/web/src/lib/builder.ts +171 -0
  170. data/web/src/lib/dcc.ts +338 -0
  171. data/web/src/lib/units.json +5581 -0
  172. data/web/src/main.ts +5 -0
  173. data/web/src/style.css +27 -0
  174. data/web/tsconfig.json +15 -0
  175. data/web/vite.config.ts +14 -0
  176. metadata +148 -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,25 +82,25 @@ 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 formulae 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]
92
+ # `repeatable` and not `type: :array`: with an array type Thor lets a
93
+ # second `-v` overwrite the first, so `-v T=42 -v R0=1` would silently
94
+ # drop T. PTB's CLI accumulates, and so must this.
95
+ method_option :variable, type: :string, aliases: "-v", repeatable: true,
96
+ banner: "NAME=VALUE",
97
+ desc: "Override a formula variable (repeatable; NAME=V1,V2 for a list)"
89
98
  def extract(target, file)
90
99
  Cli.ensure_loaded!
91
100
  dcc = Dcc.parse(File.read(file))
92
101
  case target
93
- when "files"
94
- files = ::Dcc::Extract::File.each(dcc)
95
- files = files.select { |f| f.ring.to_s == options[:ring] } if options[:ring]
96
- if options[:index]
97
- f = files[options[:index].to_i] || abort("No file at index #{options[:index]}")
98
- output_to(f.data, options[:output], f.file_name)
99
- else
100
- ::Dcc::Cli::Formatters.print_files(files)
101
- end
102
+ when "files" then extract_files(dcc)
103
+ when "formulae" then extract_formulae(dcc)
102
104
  else
103
105
  abort "Unknown extract target: #{target}"
104
106
  end
@@ -106,7 +108,7 @@ module Dcc
106
108
 
107
109
  desc "inspect FILE", "Show a human-readable summary of a DCC"
108
110
  method_option :format, type: :string, default: "text",
109
- enum: %w[text json yaml]
111
+ enum: %w[text json yaml]
110
112
  def inspect(file)
111
113
  Cli.ensure_loaded!
112
114
  dcc = Dcc.parse(File.read(file))
@@ -116,7 +118,7 @@ module Dcc
116
118
 
117
119
  desc "diff FILE1 FILE2", "Compare two DCC files structurally"
118
120
  method_option :format, type: :string, default: "text",
119
- enum: %w[text json]
121
+ enum: %w[text json]
120
122
  def diff(file1, file2)
121
123
  Cli.ensure_loaded!
122
124
  dcc1 = Dcc.parse(File.read(file1))
@@ -125,6 +127,17 @@ module Dcc
125
127
  ::Dcc::Cli::Formatters.print(diff, format: options[:format])
126
128
  end
127
129
 
130
+ desc "serve", "Start the DCC web validator (http://localhost:4567)"
131
+ method_option :port, type: :numeric, default: 4567, desc: "Port"
132
+ method_option :bind, type: :string, default: "0.0.0.0",
133
+ desc: "Bind address"
134
+ def serve
135
+ require "dcc/server"
136
+ ::Dcc.load_all!
137
+ puts "Starting DCC Validator on http://localhost:#{options[:port]}"
138
+ ::Dcc::Server::App.run!(port: options[:port], bind: options[:bind])
139
+ end
140
+
128
141
  desc "version", "Print the dcc gem version"
129
142
  def version
130
143
  puts "dcc #{::Dcc::VERSION}"
@@ -143,7 +156,120 @@ module Dcc
143
156
  $stdout.write(payload)
144
157
  end
145
158
  end
159
+
160
+ def extract_files(dcc)
161
+ files = ::Dcc::Extract::File.each(dcc)
162
+ if options[:ring]
163
+ files = files.select { |f| f.ring.to_s == options[:ring] }
164
+ end
165
+ return ::Dcc::Cli::Formatters.print_files(files) unless options[:index]
166
+
167
+ index = options[:index].to_i
168
+ file = files[index] || abort("No file at index #{index}")
169
+ output_to(file.data, options[:output], file.file_name)
170
+ end
171
+ end
172
+
173
+ no_commands do
174
+ def extract_formulae(dcc)
175
+ variables = parse_variables(options[:variable])
176
+ formulae = ::Dcc::Extract::Formula.call(dcc)
177
+ check_overrides(formulae, variables)
178
+ print_formulae(formulae, variables)
179
+ rescue ::Dcc::ExtractionError => e
180
+ abort e.message
181
+ end
182
+
183
+ # The evaluator slices bindings down to the names a formula
184
+ # actually references, so a mistyped -v reaches nothing and the
185
+ # run still reports success with plausible default numbers.
186
+ # `decimal_or_abort` already aborts on a malformed -v; an
187
+ # override that lands nowhere is the same class of mistake.
188
+ def check_overrides(formulae, variables)
189
+ unknown = variables.keys - formulae.flat_map(&:variables)
190
+ return if unknown.empty?
191
+
192
+ abort "No formula references #{unknown.join(', ')}"
193
+ end
194
+ end
195
+
196
+ no_commands do
197
+ def parse_variables(assignments)
198
+ Array(assignments).each_with_object({}) do |assignment, variables|
199
+ name, values = split_assignment(assignment)
200
+ variables[name] = override_value(values)
201
+ end
202
+ end
203
+
204
+ # `-v T=42` is a scalar and broadcasts across the document's lists;
205
+ # only `-v T=1,2` is a list, and a list has to match every other
206
+ # list's length. Wrapping a lone value in an array made `-v R0=1`
207
+ # a one-element list that collided with the document's own.
208
+ def override_value(values)
209
+ decimals = values.map { |v| decimal_or_abort(v) }
210
+ decimals.size == 1 ? decimals.first : decimals
211
+ end
212
+ end
213
+
214
+ no_commands do
215
+ # `split(",", -1)` keeps trailing empty fields, so `T=42,` reaches
216
+ # validation instead of silently losing the empty entry.
217
+ def split_assignment(assignment)
218
+ name, values = assignment.split("=", 2)
219
+ if name.nil? || name.empty? || values.nil? || values.empty?
220
+ abort "#{assignment} is not a valid NAME=value"
221
+ end
222
+
223
+ [name, values.split(",", -1)]
224
+ end
225
+
226
+ # Validated here, at the command line, not where the value is used.
227
+ # The evaluator only coerces variables the formula references, so a
228
+ # bad `-v` for an unreferenced variable — or any `-v` against a
229
+ # document with no formulae — would otherwise pass in silence.
230
+ #
231
+ # Delegates rather than calling BigDecimal itself: a second
232
+ # coercion site is exactly the drift the single boundary exists to
233
+ # prevent, even when it starts out agreeing.
234
+ def decimal_or_abort(value)
235
+ ::Dcc::Extract::Formula::Quantity.decimal(value)
236
+ rescue ::Dcc::ExtractionError
237
+ abort "#{value} is not a valid decimal value"
238
+ end
239
+ end
240
+
241
+ no_commands do
242
+ def print_formulae(formulae, variables)
243
+ return puts("No formulae found.") if formulae.empty?
244
+
245
+ # `map`, deliberately, not `all?` with a block: `all?`
246
+ # short-circuits, and stopping at the first failure is exactly
247
+ # the bug this is fixing.
248
+ printed = formulae.map { |formula| print_formula(formula, variables) }
249
+ exit(1) unless printed.all?
250
+ end
251
+ end
252
+
253
+ no_commands do
254
+ # Evaluates before printing the header. A header printed ahead
255
+ # of a failure leaves a truncated record on stdout that reads
256
+ # like a formula with no values. One formula that cannot be
257
+ # evaluated is reported on stderr and the rest still print.
258
+ def print_formula(formula, variables)
259
+ values = formula.evaluate(variables)
260
+ puts formula
261
+ values.each { |value| puts " #{value.to_s('F')}" }
262
+ true
263
+ rescue ::Dcc::ExtractionError => e
264
+ # `$stderr.puts`, not `warn`: Kernel#warn prints nothing when
265
+ # warnings are off, so `RUBYOPT=-W0` would leave the user with
266
+ # exit 1 and no explanation at all. The cop's own rationale —
267
+ # "to allow such output to be disabled" — is the thing this
268
+ # message must not permit.
269
+ $stderr.puts "#{formula}: #{e.message}" # rubocop:disable Style/StderrPuts
270
+ false
271
+ end
146
272
  end
147
273
  end
148
274
  end
149
- end
275
+ 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