openphar 0.1.0

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +221 -0
  3. data/lib/openphar/cli/main.rb +137 -0
  4. data/lib/openphar/cli.rb +11 -0
  5. data/lib/openphar/core/slug_generator.rb +107 -0
  6. data/lib/openphar/core.rb +11 -0
  7. data/lib/openphar/errors/configuration_error.rb +8 -0
  8. data/lib/openphar/errors/monograph_not_found_error.rb +15 -0
  9. data/lib/openphar/errors/parse_error.rb +18 -0
  10. data/lib/openphar/errors/unknown_format_error.rb +8 -0
  11. data/lib/openphar/errors/unknown_publisher_error.rb +8 -0
  12. data/lib/openphar/errors/unknown_type_error.rb +8 -0
  13. data/lib/openphar/errors/validation_error.rb +15 -0
  14. data/lib/openphar/errors.rb +17 -0
  15. data/lib/openphar/exporters/coverage_validator.rb +265 -0
  16. data/lib/openphar/exporters/neo4j/model_registry.rb +130 -0
  17. data/lib/openphar/exporters/neo4j/node_builder.rb +84 -0
  18. data/lib/openphar/exporters/neo4j/property_mapper.rb +154 -0
  19. data/lib/openphar/exporters/neo4j/relationship_builder.rb +87 -0
  20. data/lib/openphar/exporters/neo4j.rb +16 -0
  21. data/lib/openphar/exporters/neo4j_exporter.rb +199 -0
  22. data/lib/openphar/exporters.rb +15 -0
  23. data/lib/openphar/linkers/chp/cross_edition_linker.rb +101 -0
  24. data/lib/openphar/linkers/chp.rb +11 -0
  25. data/lib/openphar/linkers/cross_publisher_linker.rb +295 -0
  26. data/lib/openphar/linkers/herbapedia_linker.rb +125 -0
  27. data/lib/openphar/linkers.rb +13 -0
  28. data/lib/openphar/migrators/chp/classifier.rb +86 -0
  29. data/lib/openphar/migrators/chp.rb +15 -0
  30. data/lib/openphar/migrators/chp_migrator.rb +130 -0
  31. data/lib/openphar/migrators/jp_migrator.rb +345 -0
  32. data/lib/openphar/migrators/phint_migrator.rb +426 -0
  33. data/lib/openphar/migrators.rb +15 -0
  34. data/lib/openphar/models/ahp.rb +25 -0
  35. data/lib/openphar/models/amino_acid_monograph.rb +28 -0
  36. data/lib/openphar/models/api.rb +29 -0
  37. data/lib/openphar/models/assay_specification.rb +19 -0
  38. data/lib/openphar/models/base_entity.rb +221 -0
  39. data/lib/openphar/models/biological_substance_monograph.rb +31 -0
  40. data/lib/openphar/models/chemical_drug_monograph.rb +39 -0
  41. data/lib/openphar/models/chp/biologic.rb +28 -0
  42. data/lib/openphar/models/chp/chemical_preparation.rb +28 -0
  43. data/lib/openphar/models/chp/chemical_substance.rb +28 -0
  44. data/lib/openphar/models/chp/general_chapter.rb +30 -0
  45. data/lib/openphar/models/chp/monograph.rb +116 -0
  46. data/lib/openphar/models/chp/section.rb +37 -0
  47. data/lib/openphar/models/chp/tcm_crude_drug.rb +28 -0
  48. data/lib/openphar/models/chp/tcm_extract.rb +29 -0
  49. data/lib/openphar/models/chp/tcm_formulation.rb +28 -0
  50. data/lib/openphar/models/chp.rb +33 -0
  51. data/lib/openphar/models/crude_drug_monograph.rb +31 -0
  52. data/lib/openphar/models/edition.rb +41 -0
  53. data/lib/openphar/models/formulation_monograph.rb +31 -0
  54. data/lib/openphar/models/hkcmms.rb +26 -0
  55. data/lib/openphar/models/identification_specification.rb +19 -0
  56. data/lib/openphar/models/jp/kampo_formula.rb +94 -0
  57. data/lib/openphar/models/jp.rb +21 -0
  58. data/lib/openphar/models/limit.rb +54 -0
  59. data/lib/openphar/models/mineral_substance_monograph.rb +28 -0
  60. data/lib/openphar/models/monograph.rb +58 -0
  61. data/lib/openphar/models/ph_int/buffer_solution.rb +38 -0
  62. data/lib/openphar/models/ph_int/dosage_form_monograph.rb +43 -0
  63. data/lib/openphar/models/ph_int/radiopharmaceutical_monograph.rb +52 -0
  64. data/lib/openphar/models/ph_int/reagent.rb +50 -0
  65. data/lib/openphar/models/ph_int/reference_substance.rb +50 -0
  66. data/lib/openphar/models/ph_int/test_method.rb +50 -0
  67. data/lib/openphar/models/ph_int/test_solution.rb +39 -0
  68. data/lib/openphar/models/ph_int/volumetric_solution.rb +38 -0
  69. data/lib/openphar/models/ph_int.rb +37 -0
  70. data/lib/openphar/models/physical_specification.rb +17 -0
  71. data/lib/openphar/models/publisher.rb +98 -0
  72. data/lib/openphar/models/purity_specification.rb +16 -0
  73. data/lib/openphar/models/supplement.rb +31 -0
  74. data/lib/openphar/models/test_specification.rb +29 -0
  75. data/lib/openphar/models/thp.rb +24 -0
  76. data/lib/openphar/models/vitamin_monograph.rb +28 -0
  77. data/lib/openphar/models.rb +58 -0
  78. data/lib/openphar/monograph_merger.rb +374 -0
  79. data/lib/openphar/parsers/base_monograph_parser.rb +108 -0
  80. data/lib/openphar/parsers/jp_html_parser.rb +92 -0
  81. data/lib/openphar/parsers/jp_html_parser_base.rb +113 -0
  82. data/lib/openphar/parsers/jp_ja_html_parser.rb +220 -0
  83. data/lib/openphar/parsers/phint_json_parser.rb +332 -0
  84. data/lib/openphar/parsers.rb +21 -0
  85. data/lib/openphar/registry/publisher_registry.rb +46 -0
  86. data/lib/openphar/registry/type_registry.rb +147 -0
  87. data/lib/openphar/registry.rb +24 -0
  88. data/lib/openphar/repositories/monograph_repository.rb +350 -0
  89. data/lib/openphar/repositories.rb +11 -0
  90. data/lib/openphar/transformers/jsonld_transformer.rb +106 -0
  91. data/lib/openphar/transformers.rb +11 -0
  92. data/lib/openphar/version.rb +5 -0
  93. data/lib/openphar.rb +119 -0
  94. data/ontology/bibliographic/edition.ttl +267 -0
  95. data/ontology/context/chp.jsonld +69 -0
  96. data/ontology/context/crude-drugs.jsonld +56 -0
  97. data/ontology/context/pharmacopoeia.jsonld +631 -0
  98. data/ontology/core/identification.ttl +456 -0
  99. data/ontology/core/pharmacopoeia.ttl +3427 -0
  100. data/ontology/core/substance-form.ttl +302 -0
  101. data/ontology/core/unit.ttl +493 -0
  102. data/ontology/publisher/chp/extensions.ttl +186 -0
  103. data/ontology/publisher/phint/extensions.ttl +327 -0
  104. data/ontology/quality/test-method.ttl +610 -0
  105. data/shapes/chp/monograph.ttl +153 -0
  106. data/shapes/edition-shapes.ttl +230 -0
  107. data/shapes/monograph-shapes.ttl +1324 -0
  108. metadata +319 -0
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Openphar
7
+ module Migrators
8
+ # Migrates Japan Pharmacopoeia data to Open Pharmacopoeia ontology format
9
+ #
10
+ # This class handles the transformation of JP HTML monograph files into
11
+ # individual JSON-LD files and aggregate files following the OP ontology.
12
+ class JpMigrator
13
+ attr_reader :source_dir, :output_dir, :edition_id
14
+
15
+ # Category mapping for organizing monographs
16
+ CATEGORIES = {
17
+ "crude_drugs" => "crude-drugs",
18
+ "chemical_drugs" => "chemical-drugs",
19
+ "formulations" => "formulations",
20
+ "biologicals" => "biologicals",
21
+ "excipients" => "excipients",
22
+ "vitamins" => "vitamins",
23
+ "amino_acids" => "amino-acids",
24
+ "minerals" => "minerals"
25
+ }.freeze
26
+
27
+ # @param source_dir [String] Directory containing JP HTML files
28
+ # @param output_dir [String] Output directory for JSON-LD files
29
+ # @param edition_id [String] Edition IRI (default: JP18)
30
+ def initialize(source_dir:, output_dir:, edition_id: "https://www.openphar.org/data/edition/jp/jp18")
31
+ @source_dir = source_dir
32
+ @output_dir = output_dir
33
+ @edition_id = edition_id
34
+ @monographs = []
35
+ @stats = { total: 0, success: 0, failed: 0, categories: Hash.new(0) }
36
+ end
37
+
38
+ # Run the migration
39
+ # @return [Hash] Statistics about the migration
40
+ def run
41
+ puts "Starting JP migration..."
42
+ puts " Source: #{source_dir}"
43
+ puts " Output: #{output_dir}"
44
+
45
+ # Ensure output directories exist
46
+ ensure_output_directories
47
+
48
+ # Process HTML files
49
+ process_html_files
50
+
51
+ # Generate aggregate files
52
+ generate_aggregate_files
53
+
54
+ # Print statistics
55
+ print_statistics
56
+
57
+ @stats
58
+ end
59
+
60
+ private
61
+
62
+ def ensure_output_directories
63
+ CATEGORIES.each_value do |category|
64
+ dir = File.join(output_dir, "monographs", category)
65
+ FileUtils.mkdir_p(dir)
66
+ end
67
+ end
68
+
69
+ def process_html_files
70
+ html_files = Dir.glob(File.join(source_dir, "**/*.html"))
71
+ puts "Found #{html_files.count} HTML files"
72
+
73
+ html_files.each do |html_file|
74
+ process_html_file(html_file)
75
+ end
76
+ end
77
+
78
+ def process_html_file(html_file)
79
+ puts "Processing #{html_file}..."
80
+
81
+ begin
82
+ html_content = File.read(html_file, encoding: "UTF-8")
83
+ parser = Parsers::JpHtmlParser.new(html_content)
84
+ parsed_monographs = parser.parse
85
+
86
+ parsed_monographs.each do |monograph_data|
87
+ process_monograph(monograph_data)
88
+ end
89
+ rescue StandardError => e
90
+ puts " ERROR: #{e.message}"
91
+ puts " #{e.backtrace.first(5).join("\n ")}"
92
+ @stats[:failed] += 1
93
+ end
94
+ end
95
+
96
+ def process_monograph(data)
97
+ @stats[:total] += 1
98
+
99
+ # Determine category
100
+ category = determine_category(data)
101
+ @stats[:categories][category] += 1
102
+
103
+ # Generate slug from monograph name
104
+ slug = generate_slug(data[:name])
105
+
106
+ # Build JSON-LD structure
107
+ jsonld = build_monograph_jsonld(data, slug, category)
108
+
109
+ # Write individual file
110
+ output_path = File.join(output_dir, "monographs", category, "#{slug}.jsonld")
111
+ File.write(output_path, JSON.pretty_generate(jsonld))
112
+
113
+ @monographs << jsonld
114
+ @stats[:success] += 1
115
+
116
+ puts " Created: #{output_path}"
117
+ end
118
+
119
+ def determine_category(data)
120
+ # Determine based on monograph type and characteristics
121
+ return "crude-drugs" if data[:type] == "CrudeDrugMonograph"
122
+ return "formulations" if data[:dosage_form]
123
+ return "vitamins" if data[:name]&.match?(/vitamin|ascorbic|retinol|tocopherol/i)
124
+ return "amino-acids" if data[:name]&.match?(/amino acid|alanine|arginine|cysteine/i)
125
+ return "biologicals" if data[:type] == "BiologicalSubstanceMonograph"
126
+ return "excipients" if data[:name]&.match?(/lactose|starch|cellulose|silicate/i)
127
+ return "minerals" if data[:name]&.match?(/aluminum|calcium|magnesium|zinc|iron/i)
128
+
129
+ "chemical-drugs"
130
+ end
131
+
132
+ def generate_slug(name)
133
+ return "unknown-#{@stats[:total]}" unless name
134
+
135
+ name.downcase
136
+ .gsub(/[^a-z0-9\s-]/, "")
137
+ .gsub(/\s+/, "-")
138
+ .gsub(/-+/, "-")
139
+ .strip
140
+ .sub(/^-/, "")
141
+ .sub(/-$/, "")
142
+ end
143
+
144
+ def build_monograph_jsonld(data, slug, category)
145
+ base_iri = "https://www.openphar.org/data/jp/monographs/#{category}/#{slug}"
146
+
147
+ jsonld = {
148
+ "@context" => "https://www.openphar.org/ontology/context/pharmacopoeia.jsonld",
149
+ "@id" => base_iri,
150
+ "@type" => data[:type] || "ChemicalDrugMonograph",
151
+ "prefLabel" => { "en" => data[:name] },
152
+ "publisher" => "JP",
153
+ "monographId" => data[:monograph_id],
154
+ "belongsToEdition" => @edition_id
155
+ }
156
+
157
+ # Add Japanese name if available
158
+ if data[:japanese_name]
159
+ jsonld["altLabel"] = [{ "ja" => data[:japanese_name] }]
160
+ end
161
+
162
+ # Add chemical properties
163
+ if data[:molecular_formula]
164
+ jsonld["molecularFormula"] = data[:molecular_formula]
165
+ end
166
+ if data[:molecular_weight]
167
+ jsonld["molecularWeight"] = data[:molecular_weight]
168
+ end
169
+ if data[:cas_number]
170
+ jsonld["casNumber"] = data[:cas_number]
171
+ end
172
+
173
+ # Add definition
174
+ if data[:definition]
175
+ jsonld["definition"] = { "en" => data[:definition] }
176
+ end
177
+
178
+ # Add appearance
179
+ if data[:appearance]
180
+ jsonld["appearance"] = { "en" => data[:appearance] }
181
+ end
182
+
183
+ # Add test specifications
184
+ if data[:tests]&.any?
185
+ jsonld["testSpecification"] = data[:tests].map do |test|
186
+ build_test_specification(test)
187
+ end
188
+ end
189
+
190
+ # Add storage
191
+ if data[:storage]
192
+ jsonld["storageConditions"] = { "en" => data[:storage] }
193
+ end
194
+
195
+ # Add botanical source for crude drugs
196
+ if data[:botanical_source]
197
+ jsonld["botanicalSource"] = build_botanical_source(data[:botanical_source])
198
+ end
199
+
200
+ # Add macroscopic description for crude drugs
201
+ if data[:macroscopic_description]
202
+ jsonld["macroscopicDescription"] = {
203
+ "color" => { "en" => data[:macroscopic_description][:color] },
204
+ "shape" => { "en" => data[:macroscopic_description][:shape] },
205
+ "odor" => { "en" => data[:macroscopic_description][:odor] },
206
+ "taste" => { "en" => data[:macroscopic_description][:taste] }
207
+ }.compact
208
+ end
209
+
210
+ jsonld.compact
211
+ end
212
+
213
+ def build_test_specification(test)
214
+ spec = {
215
+ "@type" => test[:spec_type] || "TestSpecification",
216
+ "testName" => { "en" => test[:name] }
217
+ }
218
+
219
+ if test[:test_type]
220
+ spec["testType"] = "TestType-#{test[:test_type]}"
221
+ end
222
+
223
+ if test[:method]
224
+ spec["harmonizedMethod"] = test[:method]
225
+ end
226
+
227
+ if test[:limit]
228
+ spec["limit"] = build_limit(test[:limit])
229
+ end
230
+
231
+ if test[:conditions]
232
+ spec["testConditions"] = { "en" => test[:conditions] }
233
+ end
234
+
235
+ spec.compact
236
+ end
237
+
238
+ def build_limit(limit_data)
239
+ limit = { "@type" => "Limit" }
240
+
241
+ if limit_data[:type]
242
+ limit["limitType"] = "LimitType-#{limit_data[:type]}"
243
+ end
244
+
245
+ if limit_data[:value]
246
+ limit["limitValue"] = limit_data[:value]
247
+ end
248
+
249
+ if limit_data[:unit]
250
+ limit["limitUnit"] = limit_data[:unit]
251
+ end
252
+
253
+ if limit_data[:lower]
254
+ limit["lowerLimit"] = limit_data[:lower]
255
+ end
256
+
257
+ if limit_data[:upper]
258
+ limit["upperLimit"] = limit_data[:upper]
259
+ end
260
+
261
+ limit.compact
262
+ end
263
+
264
+ def build_botanical_source(source)
265
+ {
266
+ "@type" => "BotanicalSource",
267
+ "plantPartUsed" => { "en" => source[:part] }
268
+ }.tap do |bs|
269
+ if source[:latin_name]
270
+ bs["systematicName"] = { "la" => source[:latin_name] }
271
+ end
272
+ end
273
+ end
274
+
275
+ def generate_aggregate_files
276
+ puts "\nGenerating aggregate files..."
277
+
278
+ # Generate JSON-LD aggregate
279
+ jsonld_aggregate = {
280
+ "@context" => "https://www.openphar.org/ontology/context/pharmacopoeia.jsonld",
281
+ "@id" => "https://www.openphar.org/data/jp/jp-monographs",
282
+ "@type" => "Collection",
283
+ "prefLabel" => { "en" => "Japanese Pharmacopoeia 18 - All Monographs" },
284
+ "belongsToEdition" => @edition_id,
285
+ "@graph" => @monographs
286
+ }
287
+
288
+ jsonld_path = File.join(output_dir, "jp-monographs.jsonld")
289
+ File.write(jsonld_path, JSON.pretty_generate(jsonld_aggregate))
290
+ puts " Created: #{jsonld_path}"
291
+
292
+ # Generate TTL aggregate (via transformation)
293
+ ttl_content = generate_ttl_aggregate
294
+ ttl_path = File.join(output_dir, "jp-monographs.ttl")
295
+ File.write(ttl_path, ttl_content)
296
+ puts " Created: #{ttl_path}"
297
+ end
298
+
299
+ def generate_ttl_aggregate
300
+ # Generate a basic Turtle representation
301
+ ttl = StringIO.new
302
+ ttl.puts "@prefix op: <https://www.openphar.org/ontology/core/> ."
303
+ ttl.puts "@prefix bib: <https://www.openphar.org/ontology/bibliographic/> ."
304
+ ttl.puts "@prefix skos: <http://www.w3.org/2004/02/skos/core#> ."
305
+ ttl.puts "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> ."
306
+ ttl.puts ""
307
+ ttl.puts "<https://www.openphar.org/data/jp/jp-monographs>"
308
+ ttl.puts " a op:Collection ;"
309
+ ttl.puts " skos:prefLabel \"Japanese Pharmacopoeia 18 - All Monographs\"@en ;"
310
+ ttl.puts " op:belongsToEdition <#{@edition_id}> ;"
311
+ ttl.puts " op:monographCount #{@monographs.count} ."
312
+ ttl.puts ""
313
+
314
+ @monographs.each do |monograph|
315
+ id = monograph["@id"]
316
+ type = monograph["@type"]
317
+ label = monograph["prefLabel"]&.dig("en") || "Unknown"
318
+
319
+ ttl.puts "<#{id}>"
320
+ ttl.puts " a op:#{type} ;"
321
+ ttl.puts " skos:prefLabel #{label.inspect} ;"
322
+ ttl.puts " op:publisher op:JP ;"
323
+ ttl.puts " op:belongsToEdition <#{@edition_id}> ."
324
+ ttl.puts ""
325
+ end
326
+
327
+ ttl.string
328
+ end
329
+
330
+ def print_statistics
331
+ puts "\n" + "=" * 50
332
+ puts "Migration Statistics"
333
+ puts "=" * 50
334
+ puts "Total monographs: #{@stats[:total]}"
335
+ puts "Successfully migrated: #{@stats[:success]}"
336
+ puts "Failed: #{@stats[:failed]}"
337
+ puts "\nBy category:"
338
+ @stats[:categories].each do |category, count|
339
+ puts " #{category}: #{count}"
340
+ end
341
+ puts "=" * 50
342
+ end
343
+ end
344
+ end
345
+ end