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
@@ -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
@@ -48,4 +48,4 @@ module Dcc
48
48
  end
49
49
  end
50
50
  end
51
- end
51
+ end
@@ -0,0 +1,250 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ page_lang | default: 'en' }}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DCC {{ dcc.administrativeData.coreData.uniqueIdentifier | default: "Certificate" }}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #0a0e1a; --surface: #111827; --surface-2: #1a2236;
13
+ --border: #1e293b; --border-hi: #334155;
14
+ --text: #e2e8f0; --text-dim: #94a3b8; --text-mute: #64748b;
15
+ --accent: #38bdf8; --accent-glow: rgba(56,189,248,0.12);
16
+ --green: #4ade80; --amber: #fbbf24; --red: #f87171;
17
+ }
18
+ * { margin:0; padding:0; box-sizing:border-box; }
19
+ body { background:var(--bg); color:var(--text); font-family:'IBM Plex Sans',sans-serif; font-size:14px; line-height:1.6; -webkit-font-smoothing:antialiased; }
20
+ .mono { font-family:'IBM Plex Mono',monospace; }
21
+ .display { font-family:'DM Sans',sans-serif; letter-spacing:-0.02em; }
22
+ .page { max-width:1100px; margin:0 auto; padding:2.5rem 1.5rem 4rem; }
23
+
24
+ .header { position:relative; border:1px solid var(--border); border-radius:16px; padding:2.5rem; background:linear-gradient(135deg,var(--surface),var(--surface-2)); overflow:hidden; margin-bottom:1.5rem; }
25
+ .header::before { content:''; position:absolute; top:-50%; right:-20%; width:60%; height:200%; background:radial-gradient(ellipse,var(--accent-glow) 0%,transparent 60%); pointer-events:none; }
26
+ .header::after { content:''; position:absolute; top:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,var(--accent),transparent); opacity:0.5; }
27
+ .header-grid { display:grid; grid-template-columns:1fr auto; gap:2rem; align-items:start; position:relative; z-index:1; }
28
+ .badge { display:inline-flex; align-items:center; gap:0.5rem; padding:0.35rem 0.8rem; border:1px solid var(--accent); border-radius:100px; font-size:0.7rem; font-weight:500; letter-spacing:0.1em; text-transform:uppercase; color:var(--accent); background:var(--accent-glow); margin-bottom:1rem; }
29
+ .badge-dot { width:6px; height:6px; border-radius:50%; background:var(--accent); animation:pulse 2s ease-in-out infinite; }
30
+ @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
31
+ .header h1 { font-size:2.2rem; font-weight:700; line-height:1.1; margin-bottom:0.5rem; background:linear-gradient(180deg,#f8fafc,#94a3b8); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
32
+ .header .subtitle { color:var(--text-dim); font-size:0.95rem; }
33
+ .header .uid { font-family:'IBM Plex Mono',monospace; font-size:0.85rem; color:var(--text-mute); margin-top:0.75rem; }
34
+ .header-side { text-align:right; border-left:1px solid var(--border-hi); padding-left:2rem; }
35
+ .version-tag { font-family:'IBM Plex Mono',monospace; font-size:1.1rem; font-weight:500; color:var(--green); }
36
+ .version-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--text-mute); }
37
+
38
+ .card { border:1px solid var(--border); border-radius:12px; background:var(--surface); overflow:hidden; animation:slideUp 0.5s ease-out both; }
39
+ .card + .card { margin-top:1.5rem; }
40
+ .card:nth-child(2) { animation-delay:0.08s; }
41
+ .card:nth-child(3) { animation-delay:0.16s; }
42
+ .card:nth-child(4) { animation-delay:0.24s; }
43
+ .card:nth-child(5) { animation-delay:0.32s; }
44
+ .card:nth-child(6) { animation-delay:0.4s; }
45
+ @keyframes slideUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
46
+ .card-header { padding:1.25rem 1.5rem; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:0.75rem; }
47
+ .card-icon { width:32px; height:32px; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:1rem; border:1px solid var(--border-hi); background:var(--surface-2); }
48
+ .card-title { font-size:0.85rem; font-weight:600; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-dim); }
49
+ .card-body { padding:1.5rem; }
50
+
51
+ .data-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(250px,1fr)); gap:1px; background:var(--border); border:1px solid var(--border); border-radius:8px; overflow:hidden; }
52
+ .data-cell { background:var(--surface); padding:1rem 1.25rem; }
53
+ .data-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.1em; color:var(--text-mute); margin-bottom:0.25rem; }
54
+ .data-value { font-size:0.95rem; color:var(--text); }
55
+ .data-value.mono { font-family:'IBM Plex Mono',monospace; font-size:0.9rem; }
56
+
57
+ .data-table { width:100%; border-collapse:collapse; }
58
+ .data-table th { text-align:left; padding:0.6rem 1rem; font-size:0.7rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-mute); font-weight:500; border-bottom:1px solid var(--border-hi); }
59
+ .data-table td { padding:0.75rem 1rem; border-bottom:1px solid var(--border); font-size:0.875rem; }
60
+ .data-table tr:last-child td { border-bottom:none; }
61
+ .data-table tr:hover td { background:var(--surface-2); }
62
+
63
+ .signer { display:flex; align-items:center; gap:0.75rem; padding:0.75rem 1rem; border:1px solid var(--border); border-radius:8px; background:var(--surface-2); }
64
+ .signer + .signer { margin-top:0.5rem; }
65
+ .signer-avatar { width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:600; font-size:0.8rem; color:var(--surface); background:var(--accent); flex-shrink:0; }
66
+ .signer-name { font-weight:500; font-size:0.875rem; }
67
+ .signer-role { font-size:0.75rem; color:var(--text-mute); }
68
+ .signer-main { margin-left:auto; padding:0.2rem 0.6rem; border-radius:100px; font-size:0.65rem; font-weight:600; text-transform:uppercase; letter-spacing:0.08em; color:var(--green); border:1px solid var(--green); background:rgba(74,222,128,0.1); }
69
+
70
+ .mr-block { border:1px solid var(--border); border-radius:8px; padding:1.25rem; background:var(--surface-2); }
71
+ .mr-block + .mr-block { margin-top:0.75rem; }
72
+ .mr-title { font-weight:500; margin-bottom:0.75rem; }
73
+
74
+ @media (max-width:700px) { .header-grid { grid-template-columns:1fr; } .header-side { border-left:none; border-top:1px solid var(--border-hi); padding-left:0; padding-top:1rem; text-align:left; } }
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <div class="page">
79
+
80
+ {% assign admin = dcc.administrativeData %}
81
+ {% assign core = admin.coreData %}
82
+ {% assign lab = admin.calibrationLaboratory %}
83
+ {% assign results = dcc.measurementResults %}
84
+
85
+ <!-- Header -->
86
+ <div class="header">
87
+ <div class="header-grid">
88
+ <div>
89
+ <div class="badge"><span class="badge-dot"></span> Digital Calibration Certificate</div>
90
+ {% assign item_obj = admin.items.item %}
91
+ <h1 class="display">{{ item_obj.name.content | dcc_text | default: "Calibration Certificate" }}</h1>
92
+ <p class="subtitle">{{ lab.contact.name.content | dcc_text | default: "" }}</p>
93
+ <p class="uid mono">ID: {{ core.uniqueIdentifier | default: "—" }}</p>
94
+ </div>
95
+ <div class="header-side">
96
+ <div class="version-label">Schema</div>
97
+ <div class="version-tag mono">v{{ dcc.schemaVersion }}</div>
98
+ {% if core.countryCodeISO3166_1 %}
99
+ <div style="margin-top:0.5rem;">
100
+ <div class="version-label">Country</div>
101
+ <div class="mono" style="font-size:1.1rem;">{{ core.countryCodeISO3166_1 }}</div>
102
+ </div>
103
+ {% endif %}
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Core Data -->
109
+ <div class="card">
110
+ <div class="card-header"><div class="card-icon">&#9881;</div><span class="card-title">Core Data</span></div>
111
+ <div class="card-body">
112
+ <div class="data-grid">
113
+ <div class="data-cell"><div class="data-label">Unique ID</div><div class="data-value mono">{{ core.uniqueIdentifier | default: "—" }}</div></div>
114
+ {% if core.receiptDate %}<div class="data-cell"><div class="data-label">Receipt Date</div><div class="data-value mono">{{ core.receiptDate }}</div></div>{% endif %}
115
+ {% if core.beginPerformanceDate %}<div class="data-cell"><div class="data-label">Performance Begin</div><div class="data-value mono">{{ core.beginPerformanceDate }}</div></div>{% endif %}
116
+ {% if core.endPerformanceDate %}<div class="data-cell"><div class="data-label">Performance End</div><div class="data-value mono">{{ core.endPerformanceDate }}</div></div>{% endif %}
117
+ {% if core.usedLangCodeISO639_1 %}<div class="data-cell"><div class="data-label">Languages</div><div class="data-value mono">{{ core.usedLangCodeISO639_1 | dcc_items | join: ", " }}</div></div>{% endif %}
118
+ {% if core.mandatoryLangCodeISO639_1 %}<div class="data-cell"><div class="data-label">Mandatory Language</div><div class="data-value mono">{{ core.mandatoryLangCodeISO639_1 }}</div></div>{% endif %}
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Items -->
124
+ {% assign items_data = admin.items.item %}
125
+ {% if items_data %}
126
+ <div class="card">
127
+ <div class="card-header"><div class="card-icon">&#9776;</div><span class="card-title">Calibrated Item</span></div>
128
+ <div class="card-body">
129
+ {% if items_data.name %}
130
+ {% assign item = items_data %}
131
+ <div class="data-grid">
132
+ <div class="data-cell"><div class="data-label">Name</div><div class="data-value">{{ item.name.content | dcc_text | default: "—" }}</div></div>
133
+ {% if item.model %}<div class="data-cell"><div class="data-label">Model</div><div class="data-value">{{ item.model }}</div></div>{% endif %}
134
+ {% if item.manufacturer %}<div class="data-cell"><div class="data-label">Manufacturer</div><div class="data-value">{{ item.manufacturer.name.content | dcc_text }}</div></div>{% endif %}
135
+ </div>
136
+ {% if item.identifications %}
137
+ <table class="data-table" style="margin-top:0.75rem;">
138
+ <thead><tr><th>Identification</th><th>Issuer</th></tr></thead>
139
+ <tbody>
140
+ {% assign ids = item.identifications.identification %}
141
+ {% if ids.name %}
142
+ <tr><td class="mono">{{ ids.value }}</td><td>{{ ids.issuer }}</td></tr>
143
+ {% else %}
144
+ {% for id in ids %}<tr><td class="mono">{{ id.value }}</td><td>{{ id.issuer }}</td></tr>{% endfor %}
145
+ {% endif %}
146
+ </tbody>
147
+ </table>
148
+ {% endif %}
149
+ {% else %}
150
+ {% for item in items_data %}
151
+ <div class="data-grid">
152
+ <div class="data-cell"><div class="data-label">Name</div><div class="data-value">{{ item.name.content | dcc_text | default: "—" }}</div></div>
153
+ {% if item.model %}<div class="data-cell"><div class="data-label">Model</div><div class="data-value">{{ item.model }}</div></div>{% endif %}
154
+ {% if item.manufacturer %}<div class="data-cell"><div class="data-label">Manufacturer</div><div class="data-value">{{ item.manufacturer.name.content | dcc_text }}</div></div>{% endif %}
155
+ </div>
156
+ {% endfor %}
157
+ {% endif %}
158
+ </div>
159
+ </div>
160
+ {% endif %}
161
+
162
+ <!-- Calibration Laboratory -->
163
+ {% if lab.contact %}
164
+ <div class="card">
165
+ <div class="card-header"><div class="card-icon">&#9745;</div><span class="card-title">Calibration Laboratory</span></div>
166
+ <div class="card-body">
167
+ <div class="data-grid">
168
+ <div class="data-cell"><div class="data-label">Name</div><div class="data-value">{{ lab.contact.name.content | dcc_text }}</div></div>
169
+ {% if lab.contact.eMail %}<div class="data-cell"><div class="data-label">Email</div><div class="data-value mono">{{ lab.contact.eMail }}</div></div>{% endif %}
170
+ {% if lab.contact.phone %}<div class="data-cell"><div class="data-label">Phone</div><div class="data-value mono">{{ lab.contact.phone }}</div></div>{% endif %}
171
+ {% if lab.contact.location %}
172
+ <div class="data-cell"><div class="data-label">Address</div><div class="data-value">
173
+ {% if lab.contact.location.street %}{{ lab.contact.location.street }} {{ lab.contact.location.streetNo }}{% endif %}
174
+ {% if lab.contact.location.postCode %}<br>{{ lab.contact.location.postCode }} {{ lab.contact.location.city }}{% endif %}
175
+ {% if lab.contact.location.countryCode %}<br>{{ lab.contact.location.countryCode }}{% endif %}
176
+ </div></div>
177
+ {% endif %}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ {% endif %}
182
+
183
+ <!-- Responsible Persons -->
184
+ {% assign persons = admin.respPersons.respPerson | dcc_items %}
185
+ {% if persons.size > 0 %}
186
+ <div class="card">
187
+ <div class="card-header"><div class="card-icon">&#9794;</div><span class="card-title">Responsible Persons</span></div>
188
+ <div class="card-body">
189
+ {% for p in persons %}
190
+ {% assign pname = p.person.name.content | dcc_text %}
191
+ <div class="signer">
192
+ <div class="signer-avatar">{{ pname | dcc_initials }}</div>
193
+ <div>
194
+ <div class="signer-name">{{ pname }}</div>
195
+ {% if p.person.eMail %}<div class="signer-role mono">{{ p.person.eMail }}</div>{% endif %}
196
+ </div>
197
+ {% if p.mainSigner %}<span class="signer-main">Main Signer</span>{% endif %}
198
+ </div>
199
+ {% endfor %}
200
+ </div>
201
+ </div>
202
+ {% endif %}
203
+
204
+ <!-- Measurement Results -->
205
+ {% assign mrs = results.measurementResult | dcc_items %}
206
+ {% if mrs.size > 0 %}
207
+ <div class="card">
208
+ <div class="card-header"><div class="card-icon">&#9889;</div><span class="card-title">Measurement Results</span></div>
209
+ <div class="card-body">
210
+ {% for mr in mrs %}
211
+ <div class="mr-block">
212
+ <div class="mr-title">{{ mr.name.content | dcc_text | default: "Untitled Result" }}</div>
213
+ {% if mr.usedMethods %}
214
+ {% for method in mr.usedMethods.usedMethod | dcc_items %}
215
+ {% if method.name %}
216
+ <div style="color:var(--text-dim); font-size:0.8rem; padding-bottom:0.25rem;">{{ method.name.content | dcc_text }}</div>
217
+ {% endif %}
218
+ {% endfor %}
219
+ {% endif %}
220
+ {% if mr.results %}
221
+ {% for r in mr.results.result | dcc_items %}
222
+ {% if r.name %}
223
+ <div style="padding:0.5rem 0.75rem; border-left:2px solid var(--accent); margin-top:0.5rem; background:rgba(56,189,248,0.05); border-radius:0 6px 6px 0;">
224
+ {{ r.name.content | dcc_text }}
225
+ </div>
226
+ {% endif %}
227
+ {% endfor %}
228
+ {% endif %}
229
+ </div>
230
+ {% endfor %}
231
+ </div>
232
+ </div>
233
+ {% endif %}
234
+
235
+ <!-- Customer -->
236
+ {% if admin.customer %}
237
+ <div class="card">
238
+ <div class="card-header"><div class="card-icon">&#10148;</div><span class="card-title">Customer</span></div>
239
+ <div class="card-body">
240
+ <div class="data-grid">
241
+ <div class="data-cell"><div class="data-label">Name</div><div class="data-value">{{ admin.customer.name.content | dcc_text }}</div></div>
242
+ {% if admin.customer.eMail %}<div class="data-cell"><div class="data-label">Email</div><div class="data-value mono">{{ admin.customer.eMail }}</div></div>{% endif %}
243
+ </div>
244
+ </div>
245
+ </div>
246
+ {% endif %}
247
+
248
+ </div>
249
+ </body>
250
+ </html>
@@ -18,4 +18,4 @@ module Dcc
18
18
  end
19
19
  end
20
20
  end
21
- end
21
+ end
data/lib/dcc/convert.rb CHANGED
@@ -9,6 +9,7 @@ module Dcc
9
9
  autoload :Json, "dcc/convert/json"
10
10
  autoload :Yaml, "dcc/convert/yaml"
11
11
  autoload :Csv, "dcc/convert/csv"
12
+ autoload :LiquidFilters, "dcc/convert/liquid_filters"
12
13
  autoload :Html, "dcc/convert/html"
13
14
  end
14
15
  end
@@ -18,4 +18,4 @@ module Dcc
18
18
  end
19
19
  end
20
20
  end
21
- end
21
+ end
@@ -31,7 +31,7 @@ module Dcc
31
31
  "(no structural changes)"
32
32
  else
33
33
  header = "#{changes.size} change(s): " \
34
- "#{additions.size} added, #{removals.size} removed, #{modifications.size} changed"
34
+ "#{additions.size} added, #{removals.size} removed, #{modifications.size} changed"
35
35
  ([header] + changes.map(&:to_s)).join("\n")
36
36
  end
37
37
  end
@@ -41,7 +41,9 @@ module Dcc
41
41
  ::JSON.pretty_generate(
42
42
  additions: additions.map { |c| { path: c.path, value: c.after } },
43
43
  removals: removals.map { |c| { path: c.path, value: c.before } },
44
- modifications: modifications.map { |c| { path: c.path, before: c.before, after: c.after } },
44
+ modifications: modifications.map do |c|
45
+ { path: c.path, before: c.before, after: c.after }
46
+ end,
45
47
  )
46
48
  end
47
49
 
@@ -50,9 +52,11 @@ module Dcc
50
52
  {
51
53
  additions: additions.map { |c| { path: c.path, value: c.after } },
52
54
  removals: removals.map { |c| { path: c.path, value: c.before } },
53
- modifications: modifications.map { |c| { path: c.path, before: c.before, after: c.after } },
55
+ modifications: modifications.map do |c|
56
+ { path: c.path, before: c.before, after: c.after }
57
+ end,
54
58
  }.to_yaml
55
59
  end
56
60
  end
57
61
  end
58
- end
62
+ end
data/lib/dcc/diff.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bigdecimal"
4
+ require "date"
4
5
  require "lutaml/model"
5
6
 
6
7
  module Dcc
@@ -12,6 +13,12 @@ module Dcc
12
13
  autoload :Result, "dcc/diff/result"
13
14
  autoload :Change, "dcc/diff/change"
14
15
 
16
+ # Leaf types compared directly rather than descended into. Numeric covers
17
+ # BigDecimal and Date covers DateTime, so neither needs its own entry.
18
+ PRIMITIVE_TYPES = [
19
+ ::String, ::Numeric, ::Symbol, ::Time, ::Date
20
+ ].freeze
21
+
15
22
  class << self
16
23
  # @param a [Lutaml::Model::Serializable]
17
24
  # @param b [Lutaml::Model::Serializable]
@@ -27,20 +34,24 @@ module Dcc
27
34
  def collect_differences(a, b, path, changes)
28
35
  # Both nil - no diff
29
36
  return if a.nil? && b.nil?
37
+
30
38
  # One is nil - add/remove
31
39
  if a.nil?
32
- changes << Change.new(path: path, kind: :add, before: nil, after: summarize(b))
40
+ changes << Change.new(path: path, kind: :add, before: nil,
41
+ after: summarize(b))
33
42
  return
34
43
  end
35
44
  if b.nil?
36
- changes << Change.new(path: path, kind: :remove, before: summarize(a), after: nil)
45
+ changes << Change.new(path: path, kind: :remove,
46
+ before: summarize(a), after: nil)
37
47
  return
38
48
  end
39
49
 
40
50
  # Both primitives - compare values
41
51
  if primitive?(a) || primitive?(b)
42
- if a != b
43
- changes << Change.new(path: path, kind: :change, before: a, after: b)
52
+ unless equivalent?(a, b)
53
+ changes << Change.new(path: path, kind: :change, before: a,
54
+ after: b)
44
55
  end
45
56
  return
46
57
  end
@@ -63,14 +74,35 @@ module Dcc
63
74
  end
64
75
  end
65
76
 
77
+ # "Did this document change?" is not "are these numbers equal?".
78
+ # `BigDecimal("NaN") == BigDecimal("NaN")` is false by IEEE, and
79
+ # `Values#==` honours that, so two documents carrying the same
80
+ # not-measured entry would otherwise report a change to itself.
81
+ def equivalent?(a, b)
82
+ values = ::Dcc::Type::DecimalXmlList::Values
83
+ return a == b unless a.is_a?(values) && b.is_a?(values)
84
+
85
+ a.size == b.size && a.to_a.zip(b.to_a).all? { |x, y| same_decimal?(x, y) }
86
+ end
87
+
88
+ # `Values.new` takes whatever it is handed, so a list assigned in Ruby
89
+ # rather than parsed can hold anything. Only BigDecimals are asked
90
+ # whether they are NaN.
91
+ def same_decimal?(left, right)
92
+ return true if left == right
93
+
94
+ left.is_a?(::BigDecimal) && right.is_a?(::BigDecimal) &&
95
+ left.nan? && right.nan?
96
+ end
97
+
66
98
  def primitive?(value)
67
- return true if value.nil?
68
- return true if value.is_a?(::String) || value.is_a?(::Numeric) || value.is_a?(::Symbol)
69
- return true if value == true || value == false
70
- return true if value.is_a?(::Time) || value.is_a?(::Date) || value.is_a?(::DateTime)
71
- return true if value.is_a?(::BigDecimal)
99
+ return true if value.nil? || [true, false].include?(value)
100
+ return false if value.is_a?(::Array)
101
+ return true if PRIMITIVE_TYPES.any? { |type| value.is_a?(type) }
72
102
 
73
- false
103
+ # Anything we cannot descend into is compared with `==`, which covers
104
+ # value objects such as decimal XML lists.
105
+ !value.class.respond_to?(:attributes)
74
106
  end
75
107
 
76
108
  def summarize(node)
@@ -80,4 +112,4 @@ module Dcc
80
112
  end
81
113
  end
82
114
  end
83
- end
115
+ end