oddb2xml 3.0.0 → 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab4100aa6c522cd66ca2a69af3959c576e82cfdaeb41cffd399cb6d451ba7ff2
4
- data.tar.gz: 495bcb8a97881ba89f5a1bb9fd8acb264a56cdd8f262e1b7669579bffc9c77be
3
+ metadata.gz: c0521593bc12b423a74628f04973dcaf43241ce674c415c4e3a82da3ad34a134
4
+ data.tar.gz: a7b8fe9076dd737105696b525e3c4c0e17e3174ea4d86d0bab9f948324229c05
5
5
  SHA512:
6
- metadata.gz: f9b839faf528860fdf2ab6e319b1db88812d1aa2c7e628df45ea6de1a932c78b51b93e84b6a30355ed8a40ca4f9782d73edce2ef3742934d8b1cff99c6d19ad8
7
- data.tar.gz: a2878be3ff52ef0ee26d366f56bed4d1e443083730aa2253cbba6a1fecfcb40e01140910108649d7065dcbec3a2326208e2a8aca1918b60151df7aaea2983767
6
+ metadata.gz: 1f0c6275adc2b60930eb961b1745013f81c167a49c035347c49b40af8f59ead028f884981aeb9aa0ce809b7007a1b4c78993d2adfa5c3e31e3ed56e8866ac035
7
+ data.tar.gz: c02f68d34884adbabe1465cf7180e6ef51cbb84049a71a931880bcb383abb4c3dc8196f92e21932b3529a68e5211f546fb053112f27037434313517de4d22961
data/CLAUDE.md ADDED
@@ -0,0 +1,69 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ oddb2xml is a Ruby gem that downloads Swiss pharmaceutical data from 10+ sources (Swissmedic, BAG, Refdata, ZurRose, EPha, etc.), parses multiple formats (XML, XLSX, CSV, SOAP, fixed-width DAT), merges/deduplicates them, and generates standardized XML/DAT output files for healthcare systems. It also supports the Elexis EHR Artikelstamm format.
8
+
9
+ ## Common Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Run full test suite
16
+ bundle exec rake spec
17
+
18
+ # Run a single test file
19
+ bundle exec rspec spec/builder_spec.rb
20
+
21
+ # Run a single test by line number
22
+ bundle exec rspec spec/builder_spec.rb:42
23
+
24
+ # Lint with StandardRB
25
+ bundle exec standardrb
26
+
27
+ # Auto-fix lint issues
28
+ bundle exec standardrb --fix
29
+
30
+ # Build the gem
31
+ bundle exec rake build
32
+ ```
33
+
34
+ ## Architecture
35
+
36
+ The system follows a **download → extract → build → compress** pipeline:
37
+
38
+ 1. **CLI** (`lib/oddb2xml/cli.rb`) — Entry point. Parses options via Optimist (`options.rb`), orchestrates the pipeline, manages multi-threaded downloads.
39
+
40
+ 2. **Downloaders** (`lib/oddb2xml/downloader.rb`) — 11 subclasses of `Downloader`, each fetching from a specific Swiss data source. Files cached in `./downloads/`.
41
+
42
+ 3. **Extractors** (`lib/oddb2xml/extractor.rb`) — Matching extractor classes that parse downloaded files into Ruby hashes. Formats include XML (nokogiri/sax-machine), XLSX (rubyXL), SOAP (savon), CSV, and fixed-width text.
43
+
44
+ 4. **Builder** (`lib/oddb2xml/builder.rb`) — The largest file (~1900 lines). Merges extracted data and generates output XML/DAT files. Methods follow `prepare_*` (data assembly) and `build_*` (output generation) naming.
45
+
46
+ 5. **Calc** (`lib/oddb2xml/calc.rb`) — Composition calculation logic, works with `parslet_compositions.rb` and `compositions_syntax.rb` (Parslet-based PEG parser for drug composition strings).
47
+
48
+ 6. **Compressor** (`lib/oddb2xml/compressor.rb`) — Optional ZIP/TAR.GZ output compression.
49
+
50
+ ### Key data identifiers
51
+ - **GTIN/EAN13**: Primary article identifier (13-digit barcode)
52
+ - **Pharmacode**: Swiss pharmacy code
53
+ - **IKSNR**: Swissmedic registration number (5-digit)
54
+ - **Swissmedic sequence/pack numbers**: Combined with IKSNR to form full identifiers
55
+
56
+ ### Static data overrides
57
+ YAML files in `data/` provide manual overrides and mappings: `article_overrides.yaml`, `product_overrides.yaml`, `gtin2ignore.yaml`, `gal_forms.yaml`, `gal_groups.yaml`.
58
+
59
+ ## Testing
60
+
61
+ - Framework: RSpec with flexmock (mocking), webmock + VCR (HTTP recording/playback)
62
+ - Test fixtures: `spec/data/` (sample files), `spec/fixtures/vcr_cassettes/` (recorded HTTP responses)
63
+ - `spec/spec_helper.rb` defines test constants (GTINs) and configures VCR to avoid real HTTP calls during tests
64
+ - CI runs on Ruby 3.0, 3.1, 3.2
65
+
66
+ ## Ruby Version
67
+
68
+ - Minimum: Ruby >= 2.5.0 (gemspec)
69
+ - Current development: Ruby 3.2.0 (`.ruby-version`)
data/Gemfile CHANGED
@@ -7,5 +7,6 @@ group :debugger do
7
7
  gem "pry-doc"
8
8
  end
9
9
 
10
- gem "nokogiri", "1.13.9"
11
- gem "rack", "3.0.11"
10
+ gem "nokogiri", ">= 1.19.1"
11
+ gem "rack", ">= 3.1.20"
12
+ gem "mutex_m"
data/Gemfile.lock CHANGED
@@ -4,50 +4,50 @@ PATH
4
4
  oddb2xml (3.0.0)
5
5
  htmlentities
6
6
  httpi
7
- mechanize
7
+ mechanize (>= 2.8.5)
8
8
  minitar
9
9
  multi_json
10
- nokogiri (>= 1.8.2)
10
+ nokogiri (>= 1.19.1)
11
11
  optimist
12
12
  ox
13
13
  parslet
14
- rack (= 3.0.11)
15
- rexml
14
+ rack (>= 3.1.20)
15
+ rexml (>= 3.3.9)
16
16
  rubyXL (~> 3.4.0)
17
- rubyntlm (= 0.5.1)
17
+ rubyntlm (>= 0.6.3)
18
18
  rubyzip (~> 3.0.1)
19
19
  savon (~> 2.12.0)
20
20
  sax-machine
21
21
  spreadsheet
22
22
  standardrb
23
- webrick
23
+ webrick (>= 1.8.2)
24
24
  xml-simple
25
25
 
26
26
  GEM
27
27
  remote: https://rubygems.org/
28
28
  specs:
29
- addressable (2.8.5)
30
- public_suffix (>= 2.0.2, < 6.0)
29
+ addressable (2.8.8)
30
+ public_suffix (>= 2.0.2, < 8.0)
31
31
  akami (1.3.1)
32
32
  gyoku (>= 0.4.0)
33
33
  nokogiri
34
34
  ast (2.4.2)
35
+ base64 (0.3.0)
35
36
  builder (3.2.4)
36
37
  byebug (11.1.3)
37
38
  coderay (1.1.3)
38
- connection_pool (2.4.1)
39
+ connection_pool (3.0.2)
39
40
  crack (0.4.5)
40
41
  rexml
41
42
  diff-lcs (1.5.0)
42
- domain_name (0.5.20190701)
43
- unf (>= 0.0.5, < 1.0.0)
43
+ domain_name (0.6.20240107)
44
44
  flexmock (2.3.8)
45
45
  gyoku (1.4.0)
46
46
  builder (>= 2.1.2)
47
47
  rexml (~> 3.0)
48
48
  hashdiff (1.0.1)
49
49
  htmlentities (4.3.4)
50
- http-cookie (1.0.5)
50
+ http-cookie (1.1.0)
51
51
  domain_name (~> 0.5)
52
52
  httpi (2.5.0)
53
53
  rack
@@ -55,31 +55,37 @@ GEM
55
55
  json (2.6.3)
56
56
  language_server-protocol (3.17.0.3)
57
57
  lint_roller (1.1.0)
58
- mechanize (2.7.7)
59
- domain_name (~> 0.5, >= 0.5.1)
60
- http-cookie (~> 1.0)
61
- mime-types (>= 1.17.2)
62
- net-http-digest_auth (~> 1.1, >= 1.1.1)
63
- net-http-persistent (>= 2.5.2)
64
- nokogiri (~> 1.6)
65
- ntlm-http (~> 0.1, >= 0.1.1)
58
+ logger (1.7.0)
59
+ mechanize (2.14.0)
60
+ addressable (~> 2.8)
61
+ base64
62
+ domain_name (~> 0.5, >= 0.5.20190701)
63
+ http-cookie (~> 1.0, >= 1.0.3)
64
+ mime-types (~> 3.3)
65
+ net-http-digest_auth (~> 1.4, >= 1.4.1)
66
+ net-http-persistent (>= 2.5.2, < 5.0.dev)
67
+ nkf
68
+ nokogiri (~> 1.11, >= 1.11.2)
69
+ rubyntlm (~> 0.6, >= 0.6.3)
66
70
  webrick (~> 1.7)
67
- webrobots (>= 0.0.9, < 0.2)
71
+ webrobots (~> 0.1.2)
68
72
  method_source (1.0.0)
69
- mime-types (3.5.1)
70
- mime-types-data (~> 3.2015)
71
- mime-types-data (3.2023.0808)
72
- mini_portile2 (2.8.4)
73
+ mime-types (3.7.0)
74
+ logger
75
+ mime-types-data (~> 3.2025, >= 3.2025.0507)
76
+ mime-types-data (3.2026.0203)
77
+ mini_portile2 (2.8.9)
73
78
  minitar (0.9)
74
79
  multi_json (1.15.0)
80
+ mutex_m (0.3.0)
75
81
  net-http-digest_auth (1.4.1)
76
- net-http-persistent (4.0.2)
77
- connection_pool (~> 2.2)
78
- nokogiri (1.13.9)
79
- mini_portile2 (~> 2.8.0)
82
+ net-http-persistent (4.0.8)
83
+ connection_pool (>= 2.2.4, < 4)
84
+ nkf (0.2.0)
85
+ nokogiri (1.19.1)
86
+ mini_portile2 (~> 2.8.2)
80
87
  racc (~> 1.4)
81
88
  nori (2.6.0)
82
- ntlm-http (0.1.1)
83
89
  optimist (3.1.0)
84
90
  ox (2.14.14)
85
91
  parallel (1.23.0)
@@ -97,14 +103,14 @@ GEM
97
103
  pry (~> 0.11)
98
104
  yard (~> 0.9.11)
99
105
  psych (3.3.4)
100
- public_suffix (5.0.3)
101
- racc (1.7.1)
102
- rack (3.0.11)
106
+ public_suffix (7.0.2)
107
+ racc (1.8.1)
108
+ rack (3.2.5)
103
109
  rainbow (3.1.1)
104
110
  rake (13.0.6)
105
- rdoc (6.3.3)
111
+ rdoc (6.3.4.1)
106
112
  regexp_parser (2.8.1)
107
- rexml (3.2.6)
113
+ rexml (3.4.4)
108
114
  rspec (3.12.0)
109
115
  rspec-core (~> 3.12.0)
110
116
  rspec-expectations (~> 3.12.0)
@@ -138,7 +144,8 @@ GEM
138
144
  rubyXL (3.4.25)
139
145
  nokogiri (>= 1.10.8)
140
146
  rubyzip (>= 1.3.0)
141
- rubyntlm (0.5.1)
147
+ rubyntlm (0.6.5)
148
+ base64
142
149
  rubyzip (3.0.1)
143
150
  savon (2.12.1)
144
151
  akami (~> 1.2)
@@ -167,9 +174,6 @@ GEM
167
174
  standardrb (1.0.1)
168
175
  standard
169
176
  timecop (0.9.8)
170
- unf (0.1.4)
171
- unf_ext
172
- unf_ext (0.0.8.2)
173
177
  unicode-display_width (2.5.0)
174
178
  vcr (6.1.0)
175
179
  wasabi (3.7.0)
@@ -180,7 +184,7 @@ GEM
180
184
  addressable (>= 2.8.0)
181
185
  crack (>= 0.3.2)
182
186
  hashdiff (>= 0.4.0, < 2.0.0)
183
- webrick (1.8.1)
187
+ webrick (1.9.2)
184
188
  webrobots (0.1.2)
185
189
  xml-simple (1.1.9)
186
190
  rexml
@@ -192,14 +196,15 @@ PLATFORMS
192
196
  DEPENDENCIES
193
197
  bundler
194
198
  flexmock
195
- nokogiri (= 1.13.9)
199
+ mutex_m
200
+ nokogiri (>= 1.19.1)
196
201
  oddb2xml!
197
202
  pry-byebug
198
203
  pry-doc
199
204
  psych (< 4.0.0)
200
- rack (= 3.0.11)
205
+ rack (>= 3.1.20)
201
206
  rake
202
- rdoc (~> 6.3.3)
207
+ rdoc (>= 6.3.4.1)
203
208
  rspec
204
209
  timecop
205
210
  vcr
data/lib/oddb2xml/cli.rb CHANGED
@@ -60,14 +60,21 @@ module Oddb2xml
60
60
  threads << download(:zurrose)
61
61
  threads << download(:package) # swissmedic
62
62
  threads << download(:lppv) # oddb2xml_files
63
- threads << download(:bag) # bag.e-mediat
63
+
64
+ # Use FHIR or XML for BAG data
65
+ if @options[:fhir]
66
+ threads << download(:fhir) # FHIR from FOPH/BAG
67
+ else
68
+ threads << download(:bag) # XML from bag.e-mediat
69
+ end
70
+
64
71
  if @options[:firstbase]
65
72
  threads << download(:firstbase) # https://github.com/zdavatz/oddb2xml/issues/63
66
73
  end
67
74
  types.each do |type|
68
75
  begin
69
76
  threads << download(:refdata, type) # refdata
70
- rescue error
77
+ rescue => error
71
78
  # Should continue even when error #102
72
79
  Oddb2xml.log("Error in downloading refdata #{error}")
73
80
  end
@@ -274,6 +281,19 @@ module Oddb2xml
274
281
  @lppvs
275
282
  end
276
283
 
284
+ when :fhir
285
+ # instead of Thread.new do
286
+
287
+ downloader = FhirDownloader.new(@options)
288
+ fhir_file = downloader.download
289
+ Oddb2xml.log("FhirDownloader downloaded #{File.size(fhir_file)} bytes")
290
+ @mutex.synchronize do
291
+ hsh = FhirExtractor.new(fhir_file).to_hash
292
+ @items = hsh
293
+ Oddb2xml.log("FhirExtractor added #{@items.size} items from FHIR")
294
+ @items
295
+ end
296
+
277
297
  when :bag
278
298
  # instead of Thread.new do
279
299
 
@@ -321,7 +341,7 @@ module Oddb2xml
321
341
  @refdata_types[type] = hsh
322
342
  Oddb2xml.log("RefdataExtractor #{type} added #{hsh.size} keys now #{@refdata_types.keys} items from xml with #{xml.size} bytes")
323
343
  @refdata_types[type]
324
- rescue error
344
+ rescue => error
325
345
  # Should continue even when error https://github.com/zdavatz/oddb2xml/issues/102
326
346
  Oddb2xml.log("Error in RefdataExtractor #{error}")
327
347
  end
@@ -1,10 +1,8 @@
1
1
  require "zlib"
2
2
  require "minitar"
3
3
  require "zip"
4
-
5
4
  module Oddb2xml
6
5
  class Compressor
7
- include Archive::Tar
8
6
  attr_accessor :contents
9
7
  def initialize(prefix = "oddb", options = {})
10
8
  @options = options
@@ -0,0 +1,752 @@
1
+ # frozen_string_literal: true
2
+
3
+ # fhir_support.rb
4
+ # Complete FHIR support module for oddb2xml
5
+ # This file provides all FHIR functionality in one place
6
+
7
+ require "json"
8
+ require "date"
9
+ require "ostruct"
10
+
11
+ module Oddb2xml
12
+ # FHIR Downloader - Downloads NDJSON from FOPH/BAG
13
+ class FhirDownloader < Downloader
14
+ include DownloadMethod
15
+
16
+ BASE_URL = "https://epl.bag.admin.ch"
17
+ STATIC_FHIR_PATH = "/static/fhir"
18
+
19
+ def initialize(options = {})
20
+ @options = options
21
+ @url = find_latest_fhir_url
22
+ super(options, @url)
23
+ end
24
+
25
+ def download
26
+ filename = File.basename(@url)
27
+ file = File.join(WORK_DIR, filename)
28
+ @file2save = File.join(DOWNLOADS, filename)
29
+
30
+ report_download(@url, @file2save)
31
+
32
+ # Check if we should skip download (file exists and is recent)
33
+ if skip_download?
34
+ Oddb2xml.log "FhirDownloader: Skip downloading #{@file2save} (#{format_size(File.size(@file2save))}, less than 24h old)"
35
+ return File.expand_path(@file2save)
36
+ end
37
+
38
+ begin
39
+ # Download the file
40
+ download_as(file, "w+")
41
+
42
+ # Validate NDJSON format
43
+ if validate_ndjson(@file2save)
44
+ line_count = count_ndjson_lines(@file2save)
45
+ Oddb2xml.log "FhirDownloader: NDJSON validation successful (#{line_count} bundles, #{format_size(File.size(@file2save))})"
46
+ else
47
+ Oddb2xml.log "FhirDownloader: WARNING - NDJSON validation failed!"
48
+ end
49
+
50
+ File.expand_path(@file2save)
51
+ rescue Timeout::Error, Errno::ETIMEDOUT
52
+ retrievable? ? retry : raise
53
+ rescue => error
54
+ Oddb2xml.log "FhirDownloader: Error downloading FHIR file: #{error.message}"
55
+ raise
56
+ ensure
57
+ Oddb2xml.download_finished(@file2save, false)
58
+ FileUtils.rm_f(file, verbose: true) if File.exist?(file) && file != @file2save
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def find_latest_fhir_url
65
+ agent = Mechanize.new
66
+ response = agent.get "https://epl.bag.admin.ch/api/sl/public/resources/current"
67
+ resources = JSON.parse(response.body)
68
+ "https://epl.bag.admin.ch/static/" + resources["fhir"]["fileUrl"]
69
+ rescue => e
70
+ Oddb2xml.log "FhirDownloader: Error finding latest URL: #{e.message}"
71
+ nil
72
+ end
73
+
74
+ def skip_download?
75
+ @options[:skip_download] || (File.exist?(@file2save) && file_age_hours(@file2save) < 24)
76
+ end
77
+
78
+ def file_age_hours(file)
79
+ ((Time.now - File.ctime(file)).to_i / 3600.0).round(1)
80
+ end
81
+
82
+ def format_size(bytes)
83
+ if bytes < 1024
84
+ "#{bytes} bytes"
85
+ elsif bytes < 1024 * 1024
86
+ "#{(bytes / 1024.0).round(1)} KB"
87
+ else
88
+ "#{(bytes / (1024.0 * 1024)).round(1)} MB"
89
+ end
90
+ end
91
+
92
+ def validate_ndjson(file)
93
+ # Validate NDJSON format by checking first few lines
94
+ return false unless File.exist?(file)
95
+
96
+ begin
97
+ line_count = 0
98
+ valid_count = 0
99
+ error_lines = []
100
+
101
+ File.open(file, "r:utf-8") do |f|
102
+ # Check first 10 lines
103
+ 10.times do
104
+ line = f.gets
105
+ break if line.nil?
106
+
107
+ line_count += 1
108
+ next if line.strip.empty?
109
+
110
+ begin
111
+ data = JSON.parse(line)
112
+ # Check if it's a FHIR Bundle
113
+ if data["resourceType"] == "Bundle"
114
+ valid_count += 1
115
+ else
116
+ error_lines << "Line #{line_count}: Not a Bundle (resourceType: #{data["resourceType"]})"
117
+ end
118
+ rescue JSON::ParserError => e
119
+ error_lines << "Line #{line_count}: Invalid JSON - #{e.message}"
120
+ end
121
+ end
122
+ end
123
+
124
+ if error_lines.any?
125
+ error_lines.each { |err| Oddb2xml.log "FhirDownloader: #{err}" }
126
+ end
127
+
128
+ # Valid if we found at least some valid bundles
129
+ valid_count > 0
130
+ rescue => e
131
+ Oddb2xml.log "FhirDownloader: Validation error: #{e.message}"
132
+ false
133
+ end
134
+ end
135
+
136
+ def count_ndjson_lines(file)
137
+ # Count non-empty lines in the NDJSON file
138
+ count = 0
139
+ File.open(file, "r") do |f|
140
+ f.each_line do |line|
141
+ count += 1 unless line.strip.empty?
142
+ end
143
+ end
144
+ count
145
+ rescue => e
146
+ Oddb2xml.log "FhirDownloader: Error counting lines: #{e.message}"
147
+ 0
148
+ end
149
+ end
150
+
151
+ # FHIR Parser - Parses NDJSON and creates compatible structure
152
+ module FHIR
153
+ # Bundle represents one line in the NDJSON file
154
+ class Bundle
155
+ attr_reader :medicinal_product, :packages, :authorizations, :ingredients
156
+
157
+ def initialize(json_line)
158
+ data = JSON.parse(json_line)
159
+ @entries = data["entry"] || []
160
+ parse_entries
161
+ end
162
+
163
+ private
164
+
165
+ def parse_entries
166
+ @medicinal_product = nil
167
+ @packages = []
168
+ @authorizations = []
169
+ @ingredients = []
170
+
171
+ @entries.each do |entry|
172
+ resource = entry["resource"]
173
+ case resource["resourceType"]
174
+ when "MedicinalProductDefinition"
175
+ @medicinal_product = MedicinalProduct.new(resource)
176
+ when "PackagedProductDefinition"
177
+ @packages << Package.new(resource)
178
+ when "RegulatedAuthorization"
179
+ @authorizations << Authorization.new(resource)
180
+ when "Ingredient"
181
+ @ingredients << Ingredient.new(resource)
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ class MedicinalProduct
188
+ attr_reader :names, :atc_code, :classification, :it_codes
189
+
190
+ def initialize(resource)
191
+ @names = {}
192
+ resource["name"]&.each do |name|
193
+ lang = name.dig("usage", 0, "language", "coding", 0, "code")
194
+ @names[lang] = name["productName"]
195
+ end
196
+
197
+ # Get ATC code (classification[0])
198
+ @atc_code = resource.dig("classification", 0, "coding", 0, "code")
199
+
200
+ # Get product classification (generic/reference) (classification[1])
201
+ @classification = resource.dig("classification", 1, "coding", 0, "code")
202
+
203
+ # Get IT codes (Index Therapeuticus) from classification with correct system
204
+ @it_codes = []
205
+ resource["classification"]&.each do |cls|
206
+ cls["coding"]&.each do |coding|
207
+ if coding["system"]&.include?("index-therapeuticus")
208
+ # Format IT code from "080300" to "08.03.00" or "08.03."
209
+ it_code = format_it_code(coding["code"])
210
+ @it_codes << it_code if it_code
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def name_de
217
+ @names["de-CH"] || @names["de"]
218
+ end
219
+
220
+ def name_fr
221
+ @names["fr-CH"] || @names["fr"]
222
+ end
223
+
224
+ def name_it
225
+ @names["it-CH"] || @names["it"]
226
+ end
227
+
228
+ def it_code
229
+ # Return first IT code (primary), formatted like "08.03.00"
230
+ @it_codes.first
231
+ end
232
+
233
+ private
234
+
235
+ def format_it_code(code)
236
+ return nil unless code && code.match?(/^\d{6}$/)
237
+
238
+ # Convert "080300" to "08.03.00"
239
+ "#{code[0..1]}.#{code[2..3]}.#{code[4..5]}"
240
+ end
241
+ end
242
+
243
+ class Package
244
+ attr_reader :gtin, :description, :swissmedic_no8, :legal_status, :resource_id
245
+
246
+ def initialize(resource)
247
+ @resource_id = resource["id"]
248
+ @gtin = resource.dig("packaging", "identifier", 0, "value")
249
+ @description = resource["description"]
250
+
251
+ # Extract SwissmedicNo8 from GTIN (last 8 digits)
252
+ if @gtin && @gtin.length >= 8
253
+ @swissmedic_no8 = @gtin[-8..-1]
254
+ end
255
+
256
+ @legal_status = resource.dig("legalStatusOfSupply", 0, "code", "coding", 0, "code")
257
+ end
258
+ end
259
+
260
+ class Authorization
261
+ attr_reader :identifier, :auth_type, :holder_name, :prices, :foph_dossier_no,
262
+ :status, :listing_status, :subject_reference, :cost_share, :limitations
263
+
264
+ def initialize(resource)
265
+ @identifier = resource.dig("identifier", 0, "value")
266
+ @auth_type = resource.dig("type", "coding", 0, "display")
267
+ @holder_name = resource.dig("contained", 0, "name")
268
+ @subject_reference = resource.dig("subject", 0, "reference")
269
+ @prices = []
270
+ @foph_dossier_no = nil
271
+ @status = nil
272
+ @listing_status = nil
273
+ @cost_share = nil
274
+ @limitations = []
275
+
276
+ # Parse extensions for reimbursement info and prices
277
+ resource["extension"]&.each do |ext|
278
+ if ext["url"]&.include?("reimbursementSL")
279
+ parse_reimbursement_extension(ext)
280
+ end
281
+ end
282
+
283
+ # Parse indications for limitations
284
+ parse_indications(resource["indication"])
285
+ end
286
+
287
+ def marketing_authorization?
288
+ @auth_type == "Marketing Authorisation"
289
+ end
290
+
291
+ def reimbursement_sl?
292
+ @auth_type == "Reimbursement SL"
293
+ end
294
+
295
+ private
296
+
297
+ def parse_reimbursement_extension(ext)
298
+ ext["extension"]&.each do |sub_ext|
299
+ case sub_ext["url"]
300
+ when "FOPHDossierNumber"
301
+ @foph_dossier_no = sub_ext.dig("valueIdentifier", "value")
302
+ when "status"
303
+ @status = sub_ext.dig("valueCodeableConcept", "coding", 0, "display")
304
+ when "listingStatus"
305
+ @listing_status = sub_ext.dig("valueCodeableConcept", "coding", 0, "display")
306
+ when "costShare"
307
+ @cost_share = sub_ext["valueInteger"]
308
+ else
309
+ # Check if this is a productPrice extension (has nested extensions)
310
+ if sub_ext["url"]&.include?("productPrice")
311
+ @prices << parse_price_extension(sub_ext)
312
+ end
313
+ end
314
+ end
315
+ end
316
+
317
+ def parse_price_extension(ext)
318
+ price = {}
319
+ ext["extension"]&.each do |sub_ext|
320
+ case sub_ext["url"]
321
+ when "type"
322
+ price[:type] = sub_ext.dig("valueCodeableConcept", "coding", 0, "code")
323
+ when "value"
324
+ price[:value] = sub_ext.dig("valueMoney", "value")
325
+ price[:currency] = sub_ext.dig("valueMoney", "currency")
326
+ when "changeDate"
327
+ price[:change_date] = sub_ext["valueDate"]
328
+ when "changeType"
329
+ price[:change_type] = sub_ext.dig("valueCodeableConcept", "coding", 0, "display")
330
+ end
331
+ end
332
+ price
333
+ end
334
+
335
+ def parse_indications(indications)
336
+ return unless indications
337
+
338
+ indications.each do |indication|
339
+ indication["extension"]&.each do |ext|
340
+ if ext["url"]&.include?("regulatedAuthorization-limitation")
341
+ @limitations << parse_limitation_extension(ext)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def parse_limitation_extension(ext)
348
+ limitation = {}
349
+ ext["extension"]&.each do |sub_ext|
350
+ case sub_ext["url"]
351
+ when "status"
352
+ limitation[:status] = sub_ext.dig("valueCodeableConcept", "coding", 0, "display")
353
+ limitation[:status_code] = sub_ext.dig("valueCodeableConcept", "coding", 0, "code")
354
+ when "statusDate"
355
+ limitation[:status_date] = sub_ext["valueDate"]
356
+ when "limitationText"
357
+ limitation[:text] = sub_ext["valueString"]
358
+ when "period"
359
+ limitation[:start_date] = sub_ext.dig("valuePeriod", "start")
360
+ limitation[:end_date] = sub_ext.dig("valuePeriod", "end")
361
+ when "firstLimitationDate"
362
+ limitation[:first_date] = sub_ext["valueDate"]
363
+ end
364
+ end
365
+ limitation
366
+ end
367
+ end
368
+
369
+ class Ingredient
370
+ attr_reader :substance_name, :quantity, :unit
371
+
372
+ def initialize(resource)
373
+ @substance_name = resource.dig("substance", "code", "concept", "text")
374
+ strength = resource.dig("substance", "strength", 0)
375
+ @quantity = strength&.dig("presentationQuantity", "value")
376
+ @unit = strength&.dig("presentationQuantity", "unit")
377
+
378
+ # Handle textPresentation for ranges like "340-660"
379
+ @text_presentation = strength&.dig("textPresentation")
380
+ end
381
+
382
+ def quantity_text
383
+ @text_presentation || (@quantity ? "#{@quantity}" : "")
384
+ end
385
+ end
386
+
387
+ # Main parser class that provides compatibility with XML parser
388
+ class PreparationsParser
389
+ attr_reader :preparations
390
+
391
+ def initialize(ndjson_file)
392
+ @preparations = []
393
+ parse_file(ndjson_file)
394
+ end
395
+
396
+ def parse_file(ndjson_file)
397
+ File.foreach(ndjson_file, encoding: 'UTF-8') do |line|
398
+ next if line.strip.empty?
399
+
400
+ bundle = Bundle.new(line)
401
+ next unless bundle.medicinal_product
402
+
403
+ # Create a preparation structure compatible with XML parser output
404
+ prep = create_preparation(bundle)
405
+ @preparations << prep if prep
406
+ end
407
+ end
408
+
409
+ private
410
+
411
+ def create_preparation(bundle)
412
+ mp = bundle.medicinal_product
413
+
414
+ # Create preparation hash structure
415
+ prep = OpenStruct.new
416
+ prep.NameDe = mp.name_de
417
+ prep.NameFr = mp.name_fr
418
+ prep.NameIt = mp.name_it
419
+ prep.AtcCode = mp.atc_code
420
+ prep.OrgGenCode = map_org_gen_code(mp.classification)
421
+ prep.ItCode = mp.it_code # Add IT code
422
+
423
+ # Map packages
424
+ prep.Packs = OpenStruct.new
425
+ prep.Packs.Pack = bundle.packages.map do |pkg|
426
+ pack = OpenStruct.new
427
+ pack.GTIN = pkg.gtin
428
+ pack.SwissmedicNo8 = pkg.swissmedic_no8
429
+ pack.DescriptionDe = pkg.description
430
+ pack.DescriptionFr = pkg.description
431
+ pack.DescriptionIt = pkg.description
432
+ pack.SwissmedicCategory = map_legal_status(pkg.legal_status)
433
+
434
+ # Find prices and additional data for this package
435
+ pack.Prices = create_prices_for_package(bundle, pkg)
436
+
437
+ # Add limitations and cost share
438
+ pack.Limitations = create_limitations_for_package(bundle, pkg)
439
+ pack.CostShare = get_cost_share_for_package(bundle, pkg)
440
+
441
+ pack
442
+ end
443
+
444
+ # Map ingredients
445
+ prep.Substances = OpenStruct.new
446
+ prep.Substances.Substance = bundle.ingredients.map do |ing|
447
+ substance = OpenStruct.new
448
+ substance.DescriptionLa = ing.substance_name
449
+ substance.Quantity = ing.quantity_text
450
+ substance.QuantityUnit = ing.unit
451
+ substance
452
+ end
453
+
454
+ # Extract SwissmedicNo5 from the first authorization
455
+ marketing_auth = bundle.authorizations.find(&:marketing_authorization?)
456
+ if marketing_auth
457
+ # SwissmedicNo5 is typically the first 5 digits of the identifier
458
+ prep.SwissmedicNo5 = marketing_auth.identifier&.to_s&.[](0, 5)
459
+ end
460
+
461
+ prep
462
+ end
463
+
464
+ def create_prices_for_package(bundle, package)
465
+ prices = OpenStruct.new
466
+
467
+ # Find reimbursement authorization for this package by resource ID
468
+ # Match by ending with ID to handle both PackagedProductDefinition and CHIDMPPackagedProductDefinition
469
+ reimbursement = bundle.authorizations.find do |auth|
470
+ auth.reimbursement_sl? && auth.subject_reference&.end_with?(package.resource_id)
471
+ end
472
+
473
+ return prices unless reimbursement
474
+
475
+ reimbursement.prices.each do |price|
476
+ if price[:type] == "756002005002"
477
+ exf = OpenStruct.new
478
+ exf.Price = price[:value]
479
+ exf.ValidFromDate = price[:change_date]
480
+ exf.PriceTypeCode = price[:type]
481
+ prices.ExFactoryPrice = exf
482
+ elsif price[:type] == "756002005001"
483
+ pub = OpenStruct.new
484
+ pub.Price = price[:value]
485
+ pub.ValidFromDate = price[:change_date]
486
+ pub.PriceTypeCode = price[:type]
487
+ prices.PublicPrice = pub
488
+ end
489
+ end
490
+
491
+ prices
492
+ end
493
+
494
+ def create_limitations_for_package(bundle, package)
495
+ # Find reimbursement authorization for this package
496
+ reimbursement = bundle.authorizations.find do |auth|
497
+ auth.reimbursement_sl? && auth.subject_reference&.end_with?(package.resource_id)
498
+ end
499
+
500
+ return nil unless reimbursement
501
+ return nil if reimbursement.limitations.empty?
502
+
503
+ # Convert FHIR limitations to OpenStruct format
504
+ limitations = OpenStruct.new
505
+ limitations.Limitation = reimbursement.limitations.map do |lim|
506
+ limitation = OpenStruct.new
507
+ limitation.LimitationCode = "" # Not in FHIR
508
+ limitation.LimitationType = "" # Could derive from status
509
+ limitation.LimitationNiveau = "" # Not in FHIR
510
+ limitation.LimitationValue = "" # Not in FHIR
511
+ limitation.DescriptionDe = lim[:text] || ""
512
+ limitation.DescriptionFr = "" # May need separate language version
513
+ limitation.DescriptionIt = "" # May need separate language version
514
+ limitation.ValidFromDate = lim[:status_date] || lim[:start_date] || ""
515
+ limitation.ValidThruDate = lim[:end_date] || ""
516
+ limitation
517
+ end
518
+
519
+ limitations
520
+ end
521
+
522
+ def get_cost_share_for_package(bundle, package)
523
+ # Find reimbursement authorization for this package
524
+ reimbursement = bundle.authorizations.find do |auth|
525
+ auth.reimbursement_sl? && auth.subject_reference&.end_with?(package.resource_id)
526
+ end
527
+
528
+ reimbursement&.cost_share
529
+ end
530
+
531
+ def map_org_gen_code(classification)
532
+ return nil unless classification
533
+
534
+ case classification
535
+ when "756001003001"
536
+ "G"
537
+ when "756001003002"
538
+ "O"
539
+ else
540
+ nil
541
+ end
542
+ end
543
+
544
+ def map_legal_status(status_code)
545
+ return nil unless status_code
546
+
547
+ # Map FHIR codes to Swissmedic categories
548
+ case status_code
549
+ when "756005022001"
550
+ "A"
551
+ when "756005022003"
552
+ "B"
553
+ when "756005022005"
554
+ "C"
555
+ when "756005022007"
556
+ "D"
557
+ when "756005022009"
558
+ "E"
559
+ else
560
+ nil
561
+ end
562
+ end
563
+ end
564
+ end
565
+
566
+ # FHIR Extractor - Compatible with existing BagXmlExtractor
567
+ class FhirExtractor < Extractor
568
+ def initialize(fhir_file)
569
+ @fhir_file = fhir_file
570
+ end
571
+
572
+ def to_hash
573
+ data = {}
574
+ Oddb2xml.log "FhirExtractor: Parsing FHIR file #{@fhir_file}"
575
+
576
+ # Parse FHIR NDJSON
577
+ result = FhirPreparationsEntry.parse(@fhir_file)
578
+
579
+ result.Preparations.Preparation.each do |seq|
580
+ next unless seq
581
+ next if seq.SwissmedicNo5 && seq.SwissmedicNo5.eql?("0")
582
+
583
+ # Build item structure matching BagXmlExtractor
584
+ item = {}
585
+ item[:data_origin] = "fhir"
586
+ item[:refdata] = true
587
+ item[:product_key] = nil # Not available in FHIR
588
+ item[:desc_de] = "" # Not in FHIR at product level
589
+ item[:desc_fr] = ""
590
+ item[:desc_it] = ""
591
+ item[:name_de] = (name = seq.NameDe) ? name : ""
592
+ item[:name_fr] = (name = seq.NameFr) ? name : ""
593
+ item[:name_it] = (name = seq.NameIt) ? name : ""
594
+ item[:swissmedic_number5] = (num5 = seq.SwissmedicNo5) ? num5.to_s.rjust(5, "0") : ""
595
+ item[:org_gen_code] = (orgc = seq.OrgGenCode) ? orgc : ""
596
+ item[:deductible] = "" # Will be set per package based on cost_share
597
+ item[:deductible20] = "" # Will be set per package based on cost_share
598
+ item[:atc_code] = (atcc = seq.AtcCode) ? atcc : ""
599
+ item[:comment_de] = "" # Not available in FHIR
600
+ item[:comment_fr] = ""
601
+ item[:comment_it] = ""
602
+ item[:it_code] = (itc = seq.ItCode) ? itc : "" # NOW available in FHIR!
603
+
604
+ # Build substances array
605
+ item[:substances] = []
606
+ if seq.Substances && seq.Substances.Substance
607
+ seq.Substances.Substance.each_with_index do |sub, i|
608
+ item[:substances] << {
609
+ index: i.to_s,
610
+ name: (name = sub.DescriptionLa) ? name : "",
611
+ quantity: (qtty = sub.Quantity) ? qtty : "",
612
+ unit: (unit = sub.QuantityUnit) ? unit : ""
613
+ }
614
+ end
615
+ end
616
+
617
+ item[:pharmacodes] = []
618
+ item[:packages] = {}
619
+
620
+ # Process packages
621
+ if seq.Packs && seq.Packs.Pack
622
+ seq.Packs.Pack.each do |pac|
623
+ next unless pac.GTIN
624
+
625
+ ean13 = pac.GTIN.to_s
626
+
627
+ # Ensure SwissmedicNo8 has leading zeros
628
+ if pac.SwissmedicNo8 && pac.SwissmedicNo8.length < 8
629
+ pac.SwissmedicNo8 = pac.SwissmedicNo8.rjust(8, "0")
630
+ end
631
+
632
+ Oddb2xml.setEan13forNo8(pac.SwissmedicNo8, ean13) if pac.SwissmedicNo8
633
+
634
+ # Build price structures
635
+ exf = {price: "", valid_date: "", price_code: ""}
636
+ if pac.Prices && pac.Prices.ExFactoryPrice
637
+ exf[:price] = pac.Prices.ExFactoryPrice.Price.to_s if pac.Prices.ExFactoryPrice.Price
638
+ exf[:valid_date] = pac.Prices.ExFactoryPrice.ValidFromDate if pac.Prices.ExFactoryPrice.ValidFromDate
639
+ exf[:price_code] = "PEXF"
640
+ end
641
+
642
+ pub = {price: "", valid_date: "", price_code: ""}
643
+ if pac.Prices && pac.Prices.PublicPrice
644
+ pub[:price] = pac.Prices.PublicPrice.Price.to_s if pac.Prices.PublicPrice.Price
645
+ pub[:valid_date] = pac.Prices.PublicPrice.ValidFromDate if pac.Prices.PublicPrice.ValidFromDate
646
+ pub[:price_code] = "PPUB"
647
+ end
648
+
649
+ # Build package entry matching BagXmlExtractor structure
650
+ item[:packages][ean13] = {
651
+ ean13: ean13,
652
+ name_de: (name = seq.NameDe) ? name : "",
653
+ name_fr: (name = seq.NameFr) ? name : "",
654
+ name_it: (name = seq.NameIt) ? name : "",
655
+ desc_de: (desc = pac.DescriptionDe) ? desc : "",
656
+ desc_fr: (desc = pac.DescriptionFr) ? desc : "",
657
+ desc_it: (desc = pac.DescriptionIt) ? desc : "",
658
+ sl_entry: true,
659
+ swissmedic_category: (cat = pac.SwissmedicCategory) ? cat : "",
660
+ swissmedic_number8: (num = pac.SwissmedicNo8) ? num : "",
661
+ prices: {exf_price: exf, pub_price: pub}
662
+ }
663
+
664
+ # Map limitations from FHIR
665
+ item[:packages][ean13][:limitations] = []
666
+ if pac.Limitations && pac.Limitations.Limitation
667
+ pac.Limitations.Limitation.each do |lim|
668
+ # Calculate is_deleted safely
669
+ is_deleted = false
670
+ if lim.ValidThruDate
671
+ begin
672
+ is_deleted = Date.parse(lim.ValidThruDate) < Date.today
673
+ rescue
674
+ is_deleted = false
675
+ end
676
+ end
677
+
678
+ item[:packages][ean13][:limitations] << {
679
+ it: item[:it_code],
680
+ key: :swissmedic_number8,
681
+ id: pac.SwissmedicNo8 || "",
682
+ code: lim.LimitationCode || "",
683
+ type: lim.LimitationType || "",
684
+ value: lim.LimitationValue || "",
685
+ niv: lim.LimitationNiveau || "",
686
+ desc_de: lim.DescriptionDe || "",
687
+ desc_fr: lim.DescriptionFr || "",
688
+ desc_it: lim.DescriptionIt || "",
689
+ vdate: lim.ValidFromDate || "",
690
+ del: is_deleted
691
+ }
692
+ end
693
+ end
694
+ item[:packages][ean13][:limitation_points] = ""
695
+
696
+ # Map cost_share to deductible flags
697
+ if pac.CostShare
698
+ case pac.CostShare
699
+ when 10
700
+ item[:deductible] = "Y"
701
+ when 20
702
+ item[:deductible20] = "Y"
703
+ when 40
704
+ # New value - might need new field or special handling
705
+ item[:deductible] = "Y" # Fallback to standard deductible
706
+ end
707
+ end
708
+
709
+ # Store in data hash with ean13 as key
710
+ data[ean13] = item
711
+ end
712
+ end
713
+ end
714
+
715
+ Oddb2xml.log "FhirExtractor: Extracted #{data.size} packages"
716
+ data
717
+ end
718
+ end
719
+ end
720
+
721
+ # Compatibility layer - makes FHIR parser compatible with existing XML parser usage
722
+ class FhirPreparationsEntry
723
+ attr_reader :Preparations
724
+
725
+ def self.parse(ndjson_file)
726
+ parser = Oddb2xml::FHIR::PreparationsParser.new(ndjson_file)
727
+ entry = new
728
+ entry.Preparations = OpenStruct.new
729
+ entry.Preparations.Preparation = parser.preparations
730
+ entry
731
+ end
732
+
733
+ attr_writer :Preparations
734
+ end
735
+
736
+ # Extend PreparationsEntry to handle both XML and FHIR
737
+ class PreparationsEntry
738
+ class << self
739
+ alias_method :original_parse, :parse
740
+
741
+ def parse(input, **kwargs)
742
+ # Check if input is a file path ending in .ndjson
743
+ if input.is_a?(String) && File.exist?(input) && input.end_with?(".ndjson")
744
+ # Parse as FHIR
745
+ FhirPreparationsEntry.parse(input)
746
+ else
747
+ # Parse as XML (original behavior)
748
+ original_parse(input, **kwargs)
749
+ end
750
+ end
751
+ end
752
+ end
@@ -22,6 +22,8 @@ module Oddb2xml
22
22
  opt :extended, "pharma, non-pharma plus prices and non-pharma from zurrose.
23
23
  Products without EAN-Code will also be listed.
24
24
  File oddb_calc.xml will also be generated"
25
+ opt :fhir, "Use FHIR NDJSON format from FOPH/BAG instead of XML from Spezialitätenliste", default: false
26
+ opt :fhir_url, "Specific FHIR NDJSON URL to download (implies --fhir)", type: :string, default: nil
25
27
  opt :format, "File format F, default is xml. {xml|dat}
26
28
  If F is given, -o option is ignored.", type: :string, default: "xml"
27
29
  opt :include, "Include target option for ean14 for 'dat' format.
@@ -69,6 +71,10 @@ module Oddb2xml
69
71
  @opts[:extended] = true
70
72
  @opts[:price] = :zurrose
71
73
  end
74
+ # FHIR URL implies FHIR mode
75
+ if @opts[:fhir_url]
76
+ @opts[:fhir] = true
77
+ end
72
78
  @opts[:price] = :zurrose if @opts[:price].is_a?(TrueClass)
73
79
  @opts[:price] = @opts[:price].to_sym if @opts[:price]
74
80
  @opts[:ean14] = @opts[:include]
@@ -1,3 +1,3 @@
1
1
  module Oddb2xml
2
- VERSION = "3.0.0"
2
+ VERSION = "3.0.1"
3
3
  end
data/lib/oddb2xml.rb CHANGED
@@ -1,5 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oddb2xml/version"
4
+ require "oddb2xml/util"
5
+ require "oddb2xml/options"
6
+ require "oddb2xml/downloader"
7
+ require "oddb2xml/xml_definitions"
8
+ require "oddb2xml/extractor"
9
+ require "oddb2xml/builder"
10
+ require "oddb2xml/fhir_support"
2
11
  require "oddb2xml/cli"
3
12
 
4
13
  module Oddb2xml
14
+ class Error < StandardError; end
5
15
  end
data/oddb2xml.gemspec CHANGED
@@ -22,30 +22,30 @@ Gem::Specification.new do |spec|
22
22
  # Consulted the Gemfile.lock to get
23
23
  spec.add_dependency "rubyzip", '~> 3.0.1'
24
24
  spec.add_dependency "minitar" # , '~> 0.5.2'
25
- spec.add_dependency "mechanize" # , '~> 2.5.1'
26
- spec.add_dependency "nokogiri", ">= 1.8.2"
25
+ spec.add_dependency "mechanize", ">= 2.8.5"
26
+ spec.add_dependency "nokogiri", ">= 1.19.1"
27
27
  spec.add_dependency "savon" , '~> 2.12.0'
28
28
  spec.add_dependency "spreadsheet" # , '~> 1.0.0'
29
29
  spec.add_dependency "rubyXL", "~> 3.4.0"
30
30
  spec.add_dependency "sax-machine" # , '~> 0.1.0'
31
31
  spec.add_dependency "parslet" # , '~> 1.7.0'
32
- spec.add_dependency "rubyntlm", "0.5.1"
32
+ spec.add_dependency "rubyntlm", ">= 0.6.3"
33
33
  spec.add_dependency "multi_json" # , '>= 0.3.2'
34
34
  spec.add_dependency "httpi" # , '>= 2.4.1'
35
35
  spec.add_dependency "optimist"
36
36
  spec.add_dependency "xml-simple"
37
37
  spec.add_dependency "ox"
38
38
  spec.add_dependency "htmlentities"
39
- spec.add_dependency "webrick"
40
- spec.add_dependency "rexml"
39
+ spec.add_dependency "webrick", ">= 1.8.2"
40
+ spec.add_dependency "rexml", ">= 3.3.9"
41
41
  spec.add_dependency "standardrb"
42
- spec.add_dependency "rack", "3.0.11"
42
+ spec.add_dependency "rack", ">= 3.1.20"
43
43
 
44
44
  spec.add_development_dependency "bundler"
45
45
  spec.add_development_dependency "rake"
46
46
  spec.add_development_dependency "rspec"
47
47
  spec.add_development_dependency "webmock"
48
- spec.add_development_dependency "rdoc", "~> 6.3.3" # rdoc 6.4 depends on psych 4.0 which breaks savon!
48
+ spec.add_development_dependency "rdoc", ">= 6.3.4.1"
49
49
  spec.add_development_dependency "vcr"
50
50
  spec.add_development_dependency "timecop"
51
51
  spec.add_development_dependency "flexmock"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oddb2xml
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasuhiro Asaka, Zeno R.R. Davatz, Niklaus Giger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-21 00:00:00.000000000 Z
11
+ date: 2026-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 2.8.5
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: 2.8.5
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: nokogiri
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 1.8.2
61
+ version: 1.19.1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 1.8.2
68
+ version: 1.19.1
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: savon
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -140,16 +140,16 @@ dependencies:
140
140
  name: rubyntlm
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - '='
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: 0.5.1
145
+ version: 0.6.3
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - '='
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: 0.5.1
152
+ version: 0.6.3
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: multi_json
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -240,28 +240,28 @@ dependencies:
240
240
  requirements:
241
241
  - - ">="
242
242
  - !ruby/object:Gem::Version
243
- version: '0'
243
+ version: 1.8.2
244
244
  type: :runtime
245
245
  prerelease: false
246
246
  version_requirements: !ruby/object:Gem::Requirement
247
247
  requirements:
248
248
  - - ">="
249
249
  - !ruby/object:Gem::Version
250
- version: '0'
250
+ version: 1.8.2
251
251
  - !ruby/object:Gem::Dependency
252
252
  name: rexml
253
253
  requirement: !ruby/object:Gem::Requirement
254
254
  requirements:
255
255
  - - ">="
256
256
  - !ruby/object:Gem::Version
257
- version: '0'
257
+ version: 3.3.9
258
258
  type: :runtime
259
259
  prerelease: false
260
260
  version_requirements: !ruby/object:Gem::Requirement
261
261
  requirements:
262
262
  - - ">="
263
263
  - !ruby/object:Gem::Version
264
- version: '0'
264
+ version: 3.3.9
265
265
  - !ruby/object:Gem::Dependency
266
266
  name: standardrb
267
267
  requirement: !ruby/object:Gem::Requirement
@@ -280,16 +280,16 @@ dependencies:
280
280
  name: rack
281
281
  requirement: !ruby/object:Gem::Requirement
282
282
  requirements:
283
- - - '='
283
+ - - ">="
284
284
  - !ruby/object:Gem::Version
285
- version: 3.0.11
285
+ version: 3.1.20
286
286
  type: :runtime
287
287
  prerelease: false
288
288
  version_requirements: !ruby/object:Gem::Requirement
289
289
  requirements:
290
- - - '='
290
+ - - ">="
291
291
  - !ruby/object:Gem::Version
292
- version: 3.0.11
292
+ version: 3.1.20
293
293
  - !ruby/object:Gem::Dependency
294
294
  name: bundler
295
295
  requirement: !ruby/object:Gem::Requirement
@@ -350,16 +350,16 @@ dependencies:
350
350
  name: rdoc
351
351
  requirement: !ruby/object:Gem::Requirement
352
352
  requirements:
353
- - - "~>"
353
+ - - ">="
354
354
  - !ruby/object:Gem::Version
355
- version: 6.3.3
355
+ version: 6.3.4.1
356
356
  type: :development
357
357
  prerelease: false
358
358
  version_requirements: !ruby/object:Gem::Requirement
359
359
  requirements:
360
- - - "~>"
360
+ - - ">="
361
361
  - !ruby/object:Gem::Version
362
- version: 6.3.3
362
+ version: 6.3.4.1
363
363
  - !ruby/object:Gem::Dependency
364
364
  name: vcr
365
365
  requirement: !ruby/object:Gem::Requirement
@@ -433,6 +433,7 @@ files:
433
433
  - ".rspec"
434
434
  - ".ruby-version"
435
435
  - ".standard.yml"
436
+ - CLAUDE.md
436
437
  - Elexis_Artikelstamm_v003.xsd
437
438
  - Elexis_Artikelstamm_v5.xsd
438
439
  - Gemfile
@@ -466,6 +467,7 @@ files:
466
467
  - lib/oddb2xml/compressor.rb
467
468
  - lib/oddb2xml/downloader.rb
468
469
  - lib/oddb2xml/extractor.rb
470
+ - lib/oddb2xml/fhir_support.rb
469
471
  - lib/oddb2xml/options.rb
470
472
  - lib/oddb2xml/parslet_compositions.rb
471
473
  - lib/oddb2xml/semantic_check.rb