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
@@ -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
data/lib/dcc/error.rb CHANGED
@@ -37,9 +37,16 @@ module Dcc
37
37
  # Raised when an unsupported schema version is requested.
38
38
  class UnknownVersionError < Error; end
39
39
 
40
+ # Raised when a migration direction is not supported — any downgrade, and
41
+ # any pair whose incompatibilities have not been verified.
42
+ class UnsupportedMigrationError < Error; end
43
+
40
44
  # Raised when the builder DSL is used incorrectly.
41
45
  class BuilderError < Error; end
42
46
 
47
+ # Raised when a plugin gem's entry file cannot be loaded.
48
+ class PluginError < Error; end
49
+
43
50
  # Raised when a soft dependency (`xmldsig`, `sinatra`, etc.) is required
44
51
  # but not installed.
45
52
  class MissingDependencyError < Error
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "base64"
4
- require "set"
5
4
 
6
5
  module Dcc
7
6
  module Extract
@@ -40,7 +39,7 @@ module Dcc
40
39
  # @param dcc [Lutaml::Model::Serializable]
41
40
  # @yieldparam [Dcc::Extract::File]
42
41
  # @return [Array<Dcc::Extract::File>] the list when no block given.
43
- def each(dcc, &block)
42
+ def each(dcc, &)
44
43
  list = []
45
44
  visited = ::Set.new
46
45
 
@@ -65,7 +64,7 @@ module Dcc
65
64
  collect.call(dcc, nil)
66
65
  return list unless block_given?
67
66
 
68
- list.each(&block)
67
+ list.each(&)
69
68
  end
70
69
 
71
70
  # @param dcc [Lutaml::Model::Serializable]
@@ -140,4 +139,4 @@ module Dcc
140
139
  end
141
140
  end
142
141
  end
143
- end
142
+ end
@@ -25,4 +25,4 @@ module Dcc
25
25
  end
26
26
  end
27
27
  end
28
- end
28
+ end
data/lib/dcc/extract.rb CHANGED
@@ -6,4 +6,4 @@ module Dcc
6
6
  autoload :Ring, "dcc/extract/ring"
7
7
  autoload :Formula, "dcc/extract/formula"
8
8
  end
9
- end
9
+ end
@@ -18,7 +18,9 @@ module Dcc
18
18
  # @param lang [String, nil] requested ISO 639-1 code.
19
19
  # @return [String, nil]
20
20
  def call(text_obj, dcc: nil, lang: nil)
21
- return nil unless text_obj && Dcc::TypeGuards.has_attribute?(text_obj, :content)
21
+ return nil unless text_obj && Dcc::TypeGuards.has_attribute?(
22
+ text_obj, :content
23
+ )
22
24
 
23
25
  contents = Array(text_obj.content)
24
26
  return nil if contents.empty?
@@ -49,10 +51,12 @@ module Dcc
49
51
  end
50
52
 
51
53
  def declared_languages(dcc)
52
- return [] unless dcc && Dcc::TypeGuards.has_attribute?(dcc, :administrative_data)
54
+ return [] unless dcc && Dcc::TypeGuards.has_attribute?(dcc,
55
+ :administrative_data)
53
56
 
54
57
  admin = dcc.administrative_data
55
- return [] unless admin && Dcc::TypeGuards.has_attribute?(admin, :core_data)
58
+ return [] unless admin && Dcc::TypeGuards.has_attribute?(admin,
59
+ :core_data)
56
60
 
57
61
  core = admin.core_data
58
62
  return [] unless core
@@ -64,4 +68,4 @@ module Dcc
64
68
  end
65
69
  end
66
70
  end
67
- end
71
+ end
data/lib/dcc/i18n.rb CHANGED
@@ -9,4 +9,4 @@ module Dcc
9
9
  module I18n
10
10
  autoload :TextLookup, "dcc/i18n/text_lookup"
11
11
  end
12
- end
12
+ end
@@ -23,7 +23,7 @@ module Dcc
23
23
  def to_s
24
24
  lines = []
25
25
  lines << "DCC Inspection Report"
26
- lines << "=" * 50
26
+ lines << ("=" * 50)
27
27
  lines << "Schema version: #{schema_version}"
28
28
  lines << "Unique identifier: #{unique_identifier}"
29
29
  lines << "Country: #{country_code}"
@@ -56,7 +56,7 @@ module Dcc
56
56
  # @param dcc [Lutaml::Model::Serializable]
57
57
  # @return [Dcc::Inspect::Report]
58
58
  def call(dcc)
59
- admin = admin_data(dcc)
59
+ admin_data(dcc)
60
60
  mr_list = measurement_results(dcc)
61
61
  new(
62
62
  schema_version: dcc.schema_version.to_s,
@@ -76,42 +76,75 @@ module Dcc
76
76
  private
77
77
 
78
78
  def admin_data(dcc)
79
- Dcc::TypeGuards.has_attribute?(dcc, :administrative_data) ? dcc.administrative_data : nil
79
+ if Dcc::TypeGuards.has_attribute?(dcc,
80
+ :administrative_data)
81
+ dcc.administrative_data
82
+ end
80
83
  end
81
84
 
82
85
  def core_data(dcc)
83
86
  ad = admin_data(dcc)
84
- ad && Dcc::TypeGuards.has_attribute?(ad, :core_data) ? ad.core_data : nil
87
+ if ad && Dcc::TypeGuards.has_attribute?(ad,
88
+ :core_data)
89
+ ad.core_data
90
+ end
85
91
  end
86
92
 
87
93
  def items(dcc)
88
94
  ad = admin_data(dcc)
89
- return [] unless ad && Dcc::TypeGuards.has_attribute?(ad, :items) && ad.items && Dcc::TypeGuards.has_attribute?(ad.items, :item)
95
+ return [] unless ad && Dcc::TypeGuards.has_attribute?(ad,
96
+ :items) && ad.items && Dcc::TypeGuards.has_attribute?(
97
+ ad.items, :item
98
+ )
90
99
 
91
100
  Array(ad.items.item)
92
101
  end
93
102
 
94
103
  def measurement_results(dcc)
95
- Dcc::TypeGuards.has_attribute?(dcc, :measurement_results) && dcc.measurement_results && Dcc::TypeGuards.has_attribute?(dcc.measurement_results, :measurement_result) ?
96
- Array(dcc.measurement_results.measurement_result) : []
104
+ if Dcc::TypeGuards.has_attribute?(dcc,
105
+ :measurement_results) && dcc.measurement_results && Dcc::TypeGuards.has_attribute?(
106
+ dcc.measurement_results, :measurement_result
107
+ )
108
+ Array(dcc.measurement_results.measurement_result)
109
+ else
110
+ []
111
+ end
97
112
  end
98
113
 
99
114
  def count_quantities(mr_list)
100
115
  mr_list.sum do |mr|
101
- next 0 unless Dcc::TypeGuards.has_attribute?(mr, :results) && mr.results && Dcc::TypeGuards.has_attribute?(mr.results, :result)
116
+ next 0 unless Dcc::TypeGuards.has_attribute?(mr,
117
+ :results) && mr.results && Dcc::TypeGuards.has_attribute?(
118
+ mr.results, :result
119
+ )
102
120
 
103
121
  Array(mr.results.result).sum do |r|
104
- Dcc::TypeGuards.has_attribute?(r, :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data, :quantity) ? Array(r.data.quantity).size : 0
122
+ if Dcc::TypeGuards.has_attribute?(r,
123
+ :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data,
124
+ :quantity)
125
+ Array(r.data.quantity).size
126
+ else
127
+ 0
128
+ end
105
129
  end
106
130
  end
107
131
  end
108
132
 
109
133
  def count_lists(mr_list)
110
134
  mr_list.sum do |mr|
111
- next 0 unless Dcc::TypeGuards.has_attribute?(mr, :results) && mr.results && Dcc::TypeGuards.has_attribute?(mr.results, :result)
135
+ next 0 unless Dcc::TypeGuards.has_attribute?(mr,
136
+ :results) && mr.results && Dcc::TypeGuards.has_attribute?(
137
+ mr.results, :result
138
+ )
112
139
 
113
140
  Array(mr.results.result).sum do |r|
114
- Dcc::TypeGuards.has_attribute?(r, :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data, :list) ? Array(r.data.list).size : 0
141
+ if Dcc::TypeGuards.has_attribute?(r,
142
+ :data) && r.data && Dcc::TypeGuards.has_attribute?(r.data,
143
+ :list)
144
+ Array(r.data.list).size
145
+ else
146
+ 0
147
+ end
115
148
  end
116
149
  end
117
150
  end
@@ -120,12 +153,16 @@ module Dcc
120
153
  cd = core_data(dcc)
121
154
  return [] unless cd
122
155
 
123
- [cd.used_lang_code_iso_639_1, cd.mandatory_lang_code_iso_639_1].flatten.compact.map(&:to_s)
156
+ [cd.used_lang_code_iso_639_1,
157
+ cd.mandatory_lang_code_iso_639_1].flatten.compact.map(&:to_s)
124
158
  end
125
159
 
126
160
  def resp_persons(dcc)
127
161
  ad = admin_data(dcc)
128
- return [] unless ad && Dcc::TypeGuards.has_attribute?(ad, :resp_persons) && ad.resp_persons && Dcc::TypeGuards.has_attribute?(ad.resp_persons, :resp_person)
162
+ return [] unless ad && Dcc::TypeGuards.has_attribute?(ad,
163
+ :resp_persons) && ad.resp_persons && Dcc::TypeGuards.has_attribute?(
164
+ ad.resp_persons, :resp_person
165
+ )
129
166
 
130
167
  Array(ad.resp_persons.resp_person)
131
168
  end
@@ -136,4 +173,4 @@ module Dcc
136
173
  end
137
174
  end
138
175
  end
139
- end
176
+ end
data/lib/dcc/inspect.rb CHANGED
@@ -6,4 +6,4 @@ module Dcc
6
6
  module Inspect
7
7
  autoload :Report, "dcc/inspect/report"
8
8
  end
9
- end
9
+ end