opencdd 0.3.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c582aa6a0bf79bedf583a586f97c1c308852b53d4efc9972b3bb883a2be425fc
4
- data.tar.gz: 1195b6615ee045c3c1ac63f237ccdb930d80838db9dbe33677216e271748accc
3
+ metadata.gz: 907feca38dccee34a74c306983cdbe506bf3b18843640000501e7482cea75414
4
+ data.tar.gz: 2581c9dfac24e7bb1b8fcb973ef311854d8096947daf9a6d01ca726e672d2612
5
5
  SHA512:
6
- metadata.gz: 1cbdf2e8832ccd86b95fcfc6789d8d9dfe4be656a27f7b4cac4cb854ea6f224153f5aa8cca521362f364954e123e0c50d3424da3daf031cd73f1974823794cb9
7
- data.tar.gz: 072c326fba5a5807249b7ea79eda95c31fc42384c80de750dba20c741de0601c8c67afe61e9b58a27e594797c68b12cbb38b4166c0f56ce547130e610625c80c
6
+ metadata.gz: eb42bba181fa6e5802a6c3ed1ea46dce2d7ee9197dea2f929bd8b2e9565826cdd19dc503ba58a231b5555208244e5d8e084b92199944d8b7d30aa1e2e6879c2f
7
+ data.tar.gz: 74ab77f45f8365a433a638414c982d092140b07f9084fbb97ea94d846cc60a0c3787a29a2180d729a424715896514008c6ec0b2d5e9060ca8b3c60197d09f2fb
data/bin/lint-no-raw-mdc CHANGED
@@ -1,4 +1,6 @@
1
- #!/usr/bin/env bash
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
2
4
  # lint-no-raw-mdc — fail if any raw MDC_P### / MDC_C### / EXT_P### /
3
5
  # EXT_C### / CIM_P### string literal appears in lib/opencdd/ outside
4
6
  # the designated SSOT files.
@@ -14,29 +16,50 @@
14
16
  #
15
17
  # Comment lines (lines whose first non-whitespace character is '#')
16
18
  # are also exempt.
17
- #
18
- set -euo pipefail
19
-
20
- excludes=(
21
- ':!lib/opencdd/property_ids.rb'
22
- ':!lib/opencdd/meta_class.rb'
23
- ':!lib/opencdd/alias_table.rb'
24
- ':!lib/opencdd/parcel/sheet_schema.rb'
25
- ':!lib/opencdd/cddal/generated_parser.rb'
26
- )
27
-
28
- # Collect hits, filtering out Ruby comment-only lines.
29
- hits=$(git grep -nE '"(MDC_[CP][0-9]+(_[0-9]+)?|EXT_[CP][0-9]+|CIM_P[0-9]+)"' \
30
- -- 'lib/opencdd/**/*.rb' "${excludes[@]}" 2>/dev/null \
31
- | grep -vE '^[^:]+:[0-9]+:\s*#' \
32
- || true)
33
-
34
- if [ -n "$hits" ]; then
35
- echo "ERROR: raw MDC/EXT/CIM literal found outside designated SSOT files." >&2
36
- echo "Use Opencdd::PropertyIds::REGISTRY / Opencdd::MetaClasses::REGISTRY instead." >&2
37
- echo "" >&2
38
- echo "$hits" >&2
19
+
20
+ require "open3"
21
+
22
+ EXCLUDED_PATHS = %w[
23
+ lib/opencdd/property_ids.rb
24
+ lib/opencdd/meta_class.rb
25
+ lib/opencdd/alias_table.rb
26
+ lib/opencdd/parcel/sheet_schema.rb
27
+ lib/opencdd/cddal/generated_parser.rb
28
+ ].freeze
29
+
30
+ # Match "MDC_P###", "MDC_C###", "EXT_P###", "EXT_C###", "CIM_P###"
31
+ # with optional _### sub-ids, as a double-quoted Ruby string literal.
32
+ # Source string is POSIX ERE (not Ruby regex) — git grep -E requires it.
33
+ LITERAL_PATTERN_SOURCE = '"(MDC_[CP][0-9]+(_[0-9]+)?|EXT_[CP][0-9]+|CIM_P[0-9]+)"'.freeze
34
+
35
+ # Each git grep hit looks like: <path>:<lineno>:<content>
36
+ # Skip comment-only lines (content's first non-space char is '#').
37
+ HIT_LINE_PATTERN = /\A(?<path>[^:]+):(?<lineno>\d+):(?<content>.*)\z/m.freeze
38
+
39
+ # lib/opencdd/**/*.rb minus the excluded SSOT files.
40
+ pathspecs = ["lib/opencdd/**/*.rb"].concat(EXCLUDED_PATHS.map { |p| ":!#{p}" })
41
+
42
+ cmd = ["git", "grep", "-n", "-E", LITERAL_PATTERN_SOURCE] + ["--"] + pathspecs
43
+ stdout, _stderr, status = Open3.capture3(*cmd)
44
+
45
+ # git grep returns 1 when there are no matches, which is the success
46
+ # case. Treat anything other than 0/1 as a real failure.
47
+ unless [0, 1].include?(status.exitstatus)
48
+ warn "ERROR: git grep failed (exit #{status.exitstatus})"
49
+ exit 2
50
+ end
51
+
52
+ hits = stdout.each_line.map(&:chomp).reject do |line|
53
+ match = HIT_LINE_PATTERN.match(line)
54
+ match && match[:content].lstrip.start_with?("#")
55
+ end
56
+
57
+ if hits.any?
58
+ warn "ERROR: raw MDC/EXT/CIM literal found outside designated SSOT files."
59
+ warn "Use Opencdd::PropertyIds::REGISTRY / Opencdd::MetaClasses::REGISTRY instead."
60
+ warn ""
61
+ hits.each { |hit| warn hit }
39
62
  exit 1
40
- fi
63
+ end
41
64
 
42
65
  exit 0
data/lib/cdd.rb CHANGED
@@ -6,6 +6,6 @@
6
6
  require "opencdd"
7
7
 
8
8
  # Alias for backward compatibility. The module was renamed from
9
- # Cdd to Opencdd, but many callers (cdd-data Rakefile, specs, etc.)
9
+ # Cdd to Opencdd, but many callers (data-private Rakefile, specs, etc.)
10
10
  # still reference Cdd::. This alias lets them keep working.
11
11
  Cdd = Opencdd unless defined?(Cdd)
@@ -174,7 +174,6 @@ module Opencdd
174
174
  def format_value(value)
175
175
  s = value.to_s
176
176
  return quote_string(s) if string_literal?(s)
177
- return format_set(s) if set_like?(s)
178
177
  s
179
178
  end
180
179
 
@@ -194,15 +193,6 @@ module Opencdd
194
193
  "\"#{escaped}\""
195
194
  end
196
195
 
197
- def set_like?(s)
198
- s.start_with?("(") && s.end_with?(")")
199
- end
200
-
201
- def format_set(s)
202
- elements = Opencdd::StructuredValues.unwrap_and_split(s)
203
- "{ #{elements.join(', ')} }"
204
- end
205
-
206
196
  def symbol_name_for(entity)
207
197
  code = entity.code&.to_s
208
198
  return code if code && !code.empty?
@@ -12,6 +12,12 @@ module Opencdd
12
12
  # "what languages does this dictionary have?" — used by the browser
13
13
  # to render a language switcher and by the data pipeline to report
14
14
  # language coverage.
15
+ #
16
+ # This is a model object. It does not normalize language codes:
17
+ # source-format quirks (e.g. IEC CDD .xls files using "jp" for
18
+ # Japanese) are normalized at the SheetSchema ingestion boundary
19
+ # via +Opencdd::Parcel::LanguageAliases+, so every consumer of
20
+ # this class can assume ISO 639-1 codes unconditionally.
15
21
  class Languages
16
22
  attr_reader :source, :translations
17
23
 
@@ -58,7 +64,7 @@ module Opencdd
58
64
  def self.from_properties(properties, default_source: "en")
59
65
  langs = properties.keys.each_with_object(Set.new) do |key, acc|
60
66
  next unless key.include?(".")
61
- prefix, lang = key.split(".", 2)
67
+ _, lang = key.split(".", 2)
62
68
  next unless lang =~ /\A[a-z]{2}(-[a-z0-9]+)?\z/i
63
69
  acc << lang
64
70
  end
@@ -67,4 +73,4 @@ module Opencdd
67
73
  new(source: source, translations: translations)
68
74
  end
69
75
  end
70
- end
76
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Parcel
5
+ # Maps non-conformant language codes found in IEC CDD source .xls
6
+ # exports to their ISO 639-1 equivalents.
7
+ #
8
+ # This is a Parcel-layer concern, not a model concern: the aliases
9
+ # exist because one specific source format (IEC CDD XLS) uses
10
+ # non-standard codes in its property ID suffixes and directive
11
+ # rows. The +Languages+ value object — and every downstream
12
+ # consumer (JSON wire format, browser, TS model) — speaks ISO
13
+ # 639-1 unconditionally.
14
+ #
15
+ # The normalization happens at the SheetSchema ingestion
16
+ # boundary so downstream code never sees an alias. SheetSchema
17
+ # also exposes the set of original-to-canonical mappings it
18
+ # applied via +#normalized_language_codes+ for data-quality
19
+ # reporting.
20
+ module LanguageAliases
21
+ ALIASES = {
22
+ "jp" => "ja",
23
+ }.freeze
24
+
25
+ # Returns the ISO 639-1 form of +code+, or the input unchanged
26
+ # if it is already standard. Pure function: no I/O, no side
27
+ # effects.
28
+ def self.normalize(code)
29
+ return code if code.nil?
30
+ s = code.to_s.strip
31
+ return s if s.empty?
32
+ ALIASES.fetch(s, s)
33
+ end
34
+
35
+ # True when +code+ would be rewritten by +normalize+.
36
+ def self.alias?(code)
37
+ return false if code.nil?
38
+ ALIASES.key?(code.to_s.strip)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -22,6 +22,8 @@ module Opencdd
22
22
  # Canonicalize a Parcel column ID. Splits language tags
23
23
  # (<id>.<lang>) so they round-trip cleanly. Returns the
24
24
  # canonical ID (or the input unchanged if no mapping exists).
25
+ # Language codes are normalized to ISO 639-1 via
26
+ # +Opencdd::Parcel::LanguageAliases.normalize+.
25
27
  def self.canonical_id(raw_id)
26
28
  return nil if raw_id.nil?
27
29
  s = raw_id.to_s.strip
@@ -30,7 +32,7 @@ module Opencdd
30
32
  base = match ? match.pre_match : s
31
33
  lang = match && match[:lang]
32
34
  canonical = VARIANT_TO_CANONICAL[base] || base
33
- lang ? "#{canonical}.#{lang}" : canonical
35
+ lang ? "#{canonical}.#{Opencdd::Parcel::LanguageAliases.normalize(lang)}" : canonical
34
36
  end
35
37
 
36
38
  DIRECTIVE_ROWS = %w[
@@ -110,7 +112,7 @@ module Opencdd
110
112
 
111
113
  DIRECTIVE_ROW_PREFIX = "#".freeze
112
114
 
113
- attr_reader :columns, :columns_by_id, :column_directives
115
+ attr_reader :columns, :columns_by_id, :column_directives, :normalized_language_codes
114
116
 
115
117
  def initialize
116
118
  @columns = []
@@ -194,6 +196,7 @@ module Opencdd
194
196
  @columns_by_id[col.property_id] = col
195
197
  end
196
198
 
199
+ @normalized_language_codes = compute_normalized_language_codes.freeze
197
200
  freeze
198
201
  end
199
202
 
@@ -260,11 +263,34 @@ module Opencdd
260
263
  next if v.nil?
261
264
  s = v.to_s.strip
262
265
  next if s.empty?
266
+ lang = Opencdd::Parcel::LanguageAliases.normalize(lang)
263
267
  lang = "en" if lang.nil? || lang.empty?
264
268
  h[lang] = s
265
269
  end
266
270
  h
267
271
  end
272
+
273
+ # Audit trail of language-code normalizations applied at this
274
+ # schema's ingestion boundary. Returns a frozen Hash mapping each
275
+ # original (non-conformant) language code seen in the source to
276
+ # its ISO 639-1 equivalent. Empty when the source spoke ISO.
277
+ def compute_normalized_language_codes
278
+ seen = {}
279
+ @column_directives.each_key do |key|
280
+ _, lang = key.to_s.split(".", 2)
281
+ next if lang.nil? || lang.empty?
282
+ normalized = Opencdd::Parcel::LanguageAliases.normalize(lang)
283
+ seen[lang] = normalized if normalized != lang
284
+ end
285
+ @columns.each do |col|
286
+ next if col.raw_property_id.nil?
287
+ _, lang = col.raw_property_id.split(".", 2)
288
+ next if lang.nil? || lang.empty?
289
+ normalized = Opencdd::Parcel::LanguageAliases.normalize(lang)
290
+ seen[lang] = normalized if normalized != lang
291
+ end
292
+ seen
293
+ end
268
294
  end
269
295
  end
270
296
  end
@@ -147,7 +147,11 @@ module Opencdd
147
147
  rows = emitter.emit(built.sheet, built.entities)
148
148
  workbook.add_worksheet(name: built.sheet.name) do |ws|
149
149
  rows.each do |row|
150
- emitted = ws.add_row(row)
150
+ # All property values are strings per IEC 61360 wire format.
151
+ # Force string cell type so values like "001" survive round-trip
152
+ # (otherwise caxlsx auto-types numeric-looking strings and roo
153
+ # reads them back as integers, breaking semantically_equal?).
154
+ emitted = ws.add_row(row, types: :string)
151
155
  emitted.hidden = true if row_hidden?(row, hidden_directives)
152
156
  end
153
157
  end
@@ -7,6 +7,7 @@ module Opencdd
7
7
 
8
8
  autoload :Metadata, "opencdd/parcel/metadata"
9
9
  autoload :SheetSchema, "opencdd/parcel/sheet_schema"
10
+ autoload :LanguageAliases, "opencdd/parcel/language_aliases"
10
11
  autoload :Sheet, "opencdd/parcel/sheet"
11
12
  autoload :Workbook, "opencdd/parcel/workbook"
12
13
  autoload :WorkbookReader, "opencdd/parcel/workbook_reader"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Opencdd
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opencdd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenCDD contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-24 00:00:00.000000000 Z
11
+ date: 2026-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: roo
@@ -263,6 +263,7 @@ files:
263
263
  - lib/opencdd/parcel/csv_writer.rb
264
264
  - lib/opencdd/parcel/entity_manifest.rb
265
265
  - lib/opencdd/parcel/flat_dir_reader.rb
266
+ - lib/opencdd/parcel/language_aliases.rb
266
267
  - lib/opencdd/parcel/layout_detector.rb
267
268
  - lib/opencdd/parcel/metadata.rb
268
269
  - lib/opencdd/parcel/referenced_irdis.rb