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,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Registry
5
+ # Central registry for mapping JSON-LD @type to Ruby model classes.
6
+ #
7
+ # This registry provides a single source of truth for type mappings,
8
+ # decoupling type resolution from specific exporters (like Neo4j).
9
+ #
10
+ # ## Built-in Types
11
+ #
12
+ # The registry comes pre-populated with all core and publisher-specific
13
+ # model types. Publishers can register additional types at runtime.
14
+ #
15
+ # @example Looking up a model class
16
+ # klass = TypeRegistry.for('CrudeDrugMonograph')
17
+ # # => Openphar::Models::CrudeDrugMonograph
18
+ #
19
+ # @example Registering a custom type
20
+ # TypeRegistry.register('CustomType', MyCustomModel)
21
+ #
22
+ # @example Checking if a type is registered
23
+ # TypeRegistry.registered?('CrudeDrugMonograph') # => true
24
+ #
25
+ class TypeRegistry
26
+ class << self
27
+ # Returns the model class for a given JSON-LD @type.
28
+ #
29
+ # @param type [String] The JSON-LD @type value
30
+ # @return [Class, nil] The model class or nil if not found
31
+ def for(type)
32
+ return nil if type.nil? || type.empty?
33
+
34
+ entry = registry[type]
35
+ entry.is_a?(Proc) ? entry.call : entry
36
+ end
37
+
38
+ # Registers a new type-to-class mapping.
39
+ #
40
+ # @param type [String] The JSON-LD @type value
41
+ # @param model_class [Class, Proc] The model class or a lambda
42
+ # @return [void]
43
+ def register(type, model_class)
44
+ registry[type] = model_class
45
+ end
46
+
47
+ # Unregisters a type mapping.
48
+ #
49
+ # @param type [String] The JSON-LD @type value
50
+ # @return [void]
51
+ def unregister(type)
52
+ registry.delete(type)
53
+ end
54
+
55
+ # Checks if a type is registered.
56
+ #
57
+ # @param type [String] The JSON-LD @type value
58
+ # @return [Boolean] True if registered
59
+ def registered?(type)
60
+ registry.key?(type)
61
+ end
62
+
63
+ # Returns all registered type names.
64
+ #
65
+ # @return [Array<String>] List of registered types
66
+ def all_types
67
+ registry.keys
68
+ end
69
+
70
+ # Returns all registered model classes.
71
+ #
72
+ # @return [Array<Class>] List of model classes
73
+ def all_classes
74
+ registry.values.map { |v| v.is_a?(Proc) ? v.call : v }
75
+ end
76
+
77
+ # Clears all registered types (useful for testing).
78
+ #
79
+ # @return [void]
80
+ def clear
81
+ @registry = nil
82
+ end
83
+
84
+ # Resets to default registrations.
85
+ #
86
+ # @return [void]
87
+ def reset
88
+ clear
89
+ end
90
+
91
+ private
92
+
93
+ # Returns the registry hash, initializing with defaults if needed.
94
+ #
95
+ # @return [Hash] The type-to-class registry
96
+ def registry
97
+ @registry ||= build_default_registry
98
+ end
99
+
100
+ # Builds the default registry with all built-in types.
101
+ #
102
+ # @return [Hash] The default registry
103
+ def build_default_registry
104
+ {
105
+ # Core monograph types
106
+ 'Monograph' => lambda { Models::Monograph },
107
+ 'CrudeDrugMonograph' => lambda { Models::CrudeDrugMonograph },
108
+ 'ChemicalDrugMonograph' => lambda { Models::ChemicalDrugMonograph },
109
+ 'FormulationMonograph' => lambda { Models::FormulationMonograph },
110
+ 'VitaminMonograph' => lambda { Models::VitaminMonograph },
111
+ 'AminoAcidMonograph' => lambda { Models::AminoAcidMonograph },
112
+ 'MineralSubstanceMonograph' => lambda { Models::MineralSubstanceMonograph },
113
+ 'BiologicalSubstanceMonograph' => lambda { Models::BiologicalSubstanceMonograph },
114
+
115
+ # Test specification types
116
+ 'TestSpecification' => lambda { Models::TestSpecification },
117
+ 'AssaySpecification' => lambda { Models::AssaySpecification },
118
+ 'PuritySpecification' => lambda { Models::PuritySpecification },
119
+ 'PhysicalSpecification' => lambda { Models::PhysicalSpecification },
120
+ 'IdentificationSpecification' => lambda { Models::IdentificationSpecification },
121
+
122
+ # Bibliographic types
123
+ 'Edition' => lambda { Models::Edition },
124
+ 'Supplement' => lambda { Models::Supplement },
125
+ 'Publisher' => lambda { Models::Publisher },
126
+
127
+ # Quantitative types
128
+ 'Limit' => lambda { Models::Limit },
129
+
130
+ # JP-specific types
131
+ 'KampoFormula' => lambda { Models::JP::KampoFormula },
132
+
133
+ # Ph.Int.-specific types
134
+ 'RadiopharmaceuticalMonograph' => lambda { Models::PhInt::RadiopharmaceuticalMonograph },
135
+ 'DosageFormMonograph' => lambda { Models::PhInt::DosageFormMonograph },
136
+ 'TestMethod' => lambda { Models::PhInt::TestMethod },
137
+ 'Reagent' => lambda { Models::PhInt::Reagent },
138
+ 'TestSolution' => lambda { Models::PhInt::TestSolution },
139
+ 'VolumetricSolution' => lambda { Models::PhInt::VolumetricSolution },
140
+ 'BufferSolution' => lambda { Models::PhInt::BufferSolution },
141
+ 'ReferenceSubstance' => lambda { Models::PhInt::ReferenceSubstance }
142
+ }
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ # Registry layer for model type resolution.
5
+ #
6
+ # Provides a central registry for mapping JSON-LD @type values
7
+ # to Ruby model classes. This decouples type resolution from
8
+ # specific exporters (like Neo4j) and provides a single source
9
+ # of truth for type mappings.
10
+ #
11
+ # @example Registering a custom model type
12
+ # Openphar::Registry::TypeRegistry.register(
13
+ # 'CustomMonograph',
14
+ # Openphar::Models::CustomMonograph
15
+ # )
16
+ #
17
+ # @example Looking up a model class by type
18
+ # klass = Openphar::Registry::TypeRegistry.for('CrudeDrugMonograph')
19
+ # # => Openphar::Models::CrudeDrugMonograph
20
+ module Registry
21
+ autoload :TypeRegistry, 'openphar/registry/type_registry'
22
+ autoload :PublisherRegistry, 'openphar/registry/publisher_registry'
23
+ end
24
+ end
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'singleton'
5
+
6
+ module Openphar
7
+ module Repositories
8
+ # Central repository for monograph data access.
9
+ #
10
+ # Provides:
11
+ # - Caching of loaded monographs
12
+ # - Multiple indexes for fast lookups
13
+ # - Query interface for common operations
14
+ #
15
+ # This replaces scattered Dir.glob and JSON.parse calls throughout
16
+ # the codebase with a single, consistent data access layer.
17
+ #
18
+ # @example Loading and querying monographs
19
+ # repo = MonographRepository.instance
20
+ # repo.load_directory("./data/jp/crude-drugs")
21
+ #
22
+ # # Find by ID
23
+ # monograph = repo.find("https://www.openphar.org/data/jp/crude-drugs/ginger")
24
+ #
25
+ # # Find by CAS number
26
+ # monograph = repo.find_by_cas("50-00-0")
27
+ #
28
+ # # Get all monographs for a publisher
29
+ # jp_monographs = repo.all_for_publisher("JP")
30
+ class MonographRepository
31
+ include Singleton
32
+
33
+ # @return [Hash<String, Lutaml::Model::Serializable>] Cache of id => monograph
34
+ attr_reader :cache
35
+
36
+ # @return [Hash] Indexes for fast lookups
37
+ attr_reader :indexes
38
+
39
+ def initialize
40
+ clear
41
+ end
42
+
43
+ # Clear all cached data and indexes.
44
+ #
45
+ # @return [void]
46
+ def clear
47
+ @cache = {}
48
+ @indexes = {
49
+ cas_number: {},
50
+ latin_name: {},
51
+ japanese_name: {},
52
+ publisher: {},
53
+ edition: {},
54
+ slug: {}
55
+ }
56
+ end
57
+
58
+ # Load all monographs from a directory.
59
+ #
60
+ # @param path [String] Directory containing JSON-LD files
61
+ # @param publisher [String, nil] Optional publisher code to filter by
62
+ # @return [Integer] Number of monographs loaded
63
+ def load_directory(path, publisher: nil)
64
+ files = Dir.glob(File.join(path, '**/*.jsonld'))
65
+ count = 0
66
+
67
+ files.sort.each do |file|
68
+ count += 1 if load_file(file, publisher: publisher)
69
+ end
70
+
71
+ count
72
+ end
73
+
74
+ # Load a single monograph file.
75
+ #
76
+ # @param path [String] Path to the JSON-LD file
77
+ # @param publisher [String, nil] Optional publisher code
78
+ # @return [Lutaml::Model::Serializable, nil] The loaded monograph or nil on failure
79
+ def load_file(path, publisher: nil)
80
+ data = JSON.parse(File.read(path, encoding: 'UTF-8'))
81
+ monograph = deserialize(data)
82
+ register(monograph) if monograph
83
+ rescue JSON::ParserError => e
84
+ warn "Warning: Could not parse #{path}: #{e.message}"
85
+ nil
86
+ rescue StandardError => e
87
+ warn "Warning: Error loading #{path}: #{e.message}"
88
+ nil
89
+ end
90
+
91
+ # Register a monograph in the cache and indexes.
92
+ #
93
+ # @param monograph [Lutaml::Model::Serializable] The monograph to register
94
+ # @return [Lutaml::Model::Serializable] The registered monograph
95
+ def register(monograph)
96
+ @cache[monograph.id] = monograph
97
+ index_monograph(monograph)
98
+ monograph
99
+ end
100
+
101
+ # Find a monograph by its ID.
102
+ #
103
+ # @param id [String] The monograph's @id
104
+ # @return [Lutaml::Model::Serializable, nil] The monograph or nil if not found
105
+ def find(id)
106
+ @cache[id]
107
+ end
108
+
109
+ # Find a monograph by CAS Registry Number.
110
+ #
111
+ # @param cas_number [String] The CAS number
112
+ # @return [Lutaml::Model::Serializable, nil] The monograph or nil if not found
113
+ def find_by_cas(cas_number)
114
+ return nil unless cas_number
115
+
116
+ @indexes[:cas_number][normalize_cas(cas_number)]
117
+ end
118
+
119
+ # Find a monograph by Latin pharmaceutical name.
120
+ #
121
+ # @param name [String] The Latin name
122
+ # @return [Lutaml::Model::Serializable, nil] The monograph or nil if not found
123
+ def find_by_latin_name(name)
124
+ return nil unless name
125
+
126
+ @indexes[:latin_name][normalize_name(name)]
127
+ end
128
+
129
+ # Find a monograph by Japanese name.
130
+ #
131
+ # @param name [String] The Japanese name
132
+ # @return [Lutaml::Model::Serializable, nil] The monograph or nil if not found
133
+ def find_by_japanese_name(name)
134
+ return nil unless name
135
+
136
+ @indexes[:japanese_name][name.to_s.strip]
137
+ end
138
+
139
+ # Find a monograph by URL slug.
140
+ #
141
+ # @param slug [String] The URL slug
142
+ # @return [Lutaml::Model::Serializable, nil] The monograph or nil if not found
143
+ def find_by_slug(slug)
144
+ return nil unless slug
145
+
146
+ @indexes[:slug][slug.to_s.downcase.strip]
147
+ end
148
+
149
+ # Get all monographs for a specific publisher.
150
+ #
151
+ # @param code [String] The publisher code (e.g., "JP", "WHO")
152
+ # @return [Array<Lutaml::Model::Serializable>] Array of monographs
153
+ def all_for_publisher(code)
154
+ @indexes[:publisher][code.to_s.upcase] || []
155
+ end
156
+
157
+ # Get all monographs for a specific edition.
158
+ #
159
+ # @param edition_id [String] The edition @id
160
+ # @return [Array<Lutaml::Model::Serializable>] Array of monographs
161
+ def all_for_edition(edition_id)
162
+ @indexes[:edition][edition_id] || []
163
+ end
164
+
165
+ # Iterate over all monographs.
166
+ #
167
+ # @yield [Lutaml::Model::Serializable] Each monograph
168
+ # @return [Enumerator] If no block given
169
+ def each(&block)
170
+ return @cache.each_value unless block_given?
171
+
172
+ @cache.each_value(&block)
173
+ end
174
+
175
+ # Select monographs matching a condition.
176
+ #
177
+ # @yield [Lutaml::Model::Serializable] Each monograph
178
+ # @return [Array<Lutaml::Model::Serializable>] Matching monographs
179
+ def select(&block)
180
+ @cache.values.select(&block)
181
+ end
182
+
183
+ # Count of all cached monographs.
184
+ #
185
+ # @return [Integer]
186
+ def count
187
+ @cache.size
188
+ end
189
+
190
+ # All cached monographs.
191
+ #
192
+ # @return [Array<Lutaml::Model::Serializable>]
193
+ def all
194
+ @cache.values
195
+ end
196
+
197
+ # Check if a monograph exists.
198
+ #
199
+ # @param id [String] The monograph's @id
200
+ # @return [Boolean]
201
+ def exists?(id)
202
+ @cache.key?(id)
203
+ end
204
+
205
+ # Get monograph IDs for fuzzy matching.
206
+ #
207
+ # @return [Array<String>] All monograph IDs
208
+ def ids
209
+ @cache.keys
210
+ end
211
+
212
+ private
213
+
214
+ # Deserialize JSON data to the appropriate model class.
215
+ #
216
+ # @param data [Hash] The parsed JSON data
217
+ # @return [Lutaml::Model::Serializable, nil] The model instance or nil
218
+ def deserialize(data)
219
+ return nil unless data['@id']
220
+
221
+ type = data['@type'] || 'Monograph'
222
+ model_class = model_class_for_type(type)
223
+
224
+ return nil unless model_class
225
+
226
+ # Use Lutaml::Model's from_json method
227
+ model_class.from_json(data.to_json)
228
+ rescue StandardError => e
229
+ warn "Warning: Could not deserialize #{data['@id']}: #{e.message}"
230
+ nil
231
+ end
232
+
233
+ # Get the model class for a JSON-LD type.
234
+ #
235
+ # @param type [String] The @type value
236
+ # @return [Class, nil] The model class
237
+ def model_class_for_type(type)
238
+ # Try registry first (if available)
239
+ if defined?(Registry::TypeRegistry)
240
+ model_class = Registry::TypeRegistry.model_class_for(type)
241
+ return model_class if model_class
242
+ end
243
+
244
+ # Fallback to known types
245
+ type_to_class_map[type]
246
+ end
247
+
248
+ # Map of type names to model classes.
249
+ #
250
+ # @return [Hash<String, Class>]
251
+ def type_to_class_map
252
+ @type_to_class_map ||= {
253
+ # Core monograph types
254
+ 'Monograph' => Models::Monograph,
255
+ 'CrudeDrugMonograph' => Models::CrudeDrugMonograph,
256
+ 'ChemicalDrugMonograph' => Models::ChemicalDrugMonograph,
257
+ 'FormulationMonograph' => Models::FormulationMonograph,
258
+ 'VitaminMonograph' => Models::VitaminMonograph,
259
+ 'AminoAcidMonograph' => Models::AminoAcidMonograph,
260
+ 'MineralSubstanceMonograph' => Models::MineralSubstanceMonograph,
261
+ 'BiologicalSubstanceMonograph' => Models::BiologicalSubstanceMonograph,
262
+ # JP types
263
+ 'KampoFormula' => Models::JP::KampoFormula,
264
+ # Ph.Int. types
265
+ 'RadiopharmaceuticalMonograph' => Models::PhInt::RadiopharmaceuticalMonograph,
266
+ 'DosageFormMonograph' => Models::PhInt::DosageFormMonograph,
267
+ 'TestMethod' => Models::PhInt::TestMethod,
268
+ 'Reagent' => Models::PhInt::Reagent,
269
+ 'TestSolution' => Models::PhInt::TestSolution,
270
+ 'VolumetricSolution' => Models::PhInt::VolumetricSolution,
271
+ 'BufferSolution' => Models::PhInt::BufferSolution,
272
+ 'ReferenceSubstance' => Models::PhInt::ReferenceSubstance
273
+ }
274
+ end
275
+
276
+ # Index a monograph for fast lookups.
277
+ #
278
+ # @param monograph [Lutaml::Model::Serializable] The monograph to index
279
+ def index_monograph(monograph)
280
+ # Index by CAS number
281
+ index_by_cas(monograph)
282
+
283
+ # Index by names
284
+ index_by_names(monograph)
285
+
286
+ # Index by publisher
287
+ index_by_publisher(monograph)
288
+
289
+ # Index by edition
290
+ index_by_edition(monograph)
291
+
292
+ # Index by slug
293
+ index_by_slug(monograph)
294
+ end
295
+
296
+ def index_by_cas(monograph)
297
+ return unless monograph.respond_to?(:cas_number) && monograph.cas_number
298
+
299
+ key = normalize_cas(monograph.cas_number)
300
+ @indexes[:cas_number][key] = monograph
301
+ end
302
+
303
+ def index_by_names(monograph)
304
+ return unless monograph.respond_to?(:pref_label)
305
+
306
+ # Latin name
307
+ if monograph.pref_label['la']
308
+ key = normalize_name(monograph.pref_label['la'])
309
+ @indexes[:latin_name][key] = monograph
310
+ end
311
+
312
+ # Japanese name
313
+ return unless monograph.pref_label['ja']
314
+
315
+ key = monograph.pref_label['ja'].to_s.strip
316
+ @indexes[:japanese_name][key] = monograph
317
+ end
318
+
319
+ def index_by_publisher(monograph)
320
+ return unless monograph.respond_to?(:publisher) && monograph.publisher
321
+
322
+ key = monograph.publisher.to_s.upcase
323
+ (@indexes[:publisher][key] ||= []) << monograph
324
+ end
325
+
326
+ def index_by_edition(monograph)
327
+ return unless monograph.respond_to?(:belongs_to_edition) && monograph.belongs_to_edition
328
+
329
+ key = monograph.belongs_to_edition
330
+ (@indexes[:edition][key] ||= []) << monograph
331
+ end
332
+
333
+ def index_by_slug(monograph)
334
+ return unless monograph.id
335
+
336
+ # Extract slug from ID
337
+ slug = monograph.id.split('/').last
338
+ @indexes[:slug][slug.downcase] = monograph
339
+ end
340
+
341
+ def normalize_cas(cas)
342
+ cas.to_s.strip
343
+ end
344
+
345
+ def normalize_name(name)
346
+ name.to_s.downcase.gsub(/[^a-z0-9]/, '')
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Repositories
5
+ # Repository pattern for data access.
6
+ #
7
+ # Provides caching, indexing, and query interfaces for monograph data.
8
+
9
+ autoload :MonographRepository, 'openphar/repositories/monograph_repository'
10
+ end
11
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Openphar
6
+ module Transformers
7
+ # Transforms parsed monograph data to JSON-LD format
8
+ class JsonldTransformer
9
+ attr_reader :context, :base_iri
10
+
11
+ def initialize(context: nil, base_iri: nil)
12
+ @context = context || default_context
13
+ @base_iri = base_iri || Openphar.base_iri
14
+ end
15
+
16
+ # Transform a monograph hash to JSON-LD
17
+ def transform(monograph_data, publisher: nil, edition: nil)
18
+ {
19
+ "@context" => context,
20
+ "@id" => monograph_iri(monograph_data[:id], publisher),
21
+ "@type" => monograph_type(monograph_data),
22
+ "prefLabel" => build_labels(monograph_data),
23
+ "monographId" => monograph_data[:id],
24
+ "publisher" => publisher_iri(publisher),
25
+ "belongsToEdition" => edition_iri(edition),
26
+ "definition" => { "en" => monograph_data[:definition] },
27
+ "testSpecification" => build_test_specifications(monograph_data)
28
+ }.compact
29
+ end
30
+
31
+ # Transform multiple monographs to a JSON-LD graph
32
+ def transform_graph(monographs_data, publisher: nil, edition: nil)
33
+ {
34
+ "@context" => context,
35
+ "@graph" => monographs_data.map do |data|
36
+ transform(data, publisher: publisher, edition: edition)
37
+ end
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def default_context
44
+ "#{base_iri}/ontology/context/pharmacopoeia.jsonld"
45
+ end
46
+
47
+ def monograph_iri(id, publisher)
48
+ slug = id.to_s.downcase.gsub(/\s+/, "-").gsub(/[^a-z0-9-]/, "")
49
+ "#{base_iri}/data/#{publisher.downcase}/monographs/#{slug}"
50
+ end
51
+
52
+ def publisher_iri(publisher)
53
+ "#{base_iri}/ontology/core/#{publisher}"
54
+ end
55
+
56
+ def edition_iri(edition)
57
+ return nil unless edition
58
+
59
+ "#{base_iri}/data/edition/#{edition.downcase}"
60
+ end
61
+
62
+ def monograph_type(data)
63
+ # Determine monograph type based on content
64
+ if data[:botanical_source] || data[:macroscopic_description]
65
+ "CrudeDrugMonograph"
66
+ elsif data[:molecular_formula]
67
+ "ChemicalDrugMonograph"
68
+ else
69
+ "PharmacopoeiaMonograph"
70
+ end
71
+ end
72
+
73
+ def build_labels(data)
74
+ labels = { "en" => data[:name] }
75
+ labels["ja"] = data[:japanese_name] if data[:japanese_name]
76
+ labels.compact
77
+ end
78
+
79
+ def build_test_specifications(data)
80
+ specs = []
81
+
82
+ # Add assay specification
83
+ if data[:assay]
84
+ specs << {
85
+ "@type" => "AssaySpecification",
86
+ "testName" => { "en" => "Assay" },
87
+ "testType" => "TestType-Assay",
88
+ "testDescription" => { "en" => data[:assay] }
89
+ }
90
+ end
91
+
92
+ # Add purity specification
93
+ if data[:purity]
94
+ specs << {
95
+ "@type" => "PuritySpecification",
96
+ "testName" => { "en" => "Purity" },
97
+ "testType" => "TestType-Purity",
98
+ "testDescription" => { "en" => data[:purity] }
99
+ }
100
+ end
101
+
102
+ specs.empty? ? nil : specs
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Transformers
5
+ # Transformers for converting data between formats.
6
+ #
7
+ # Handles conversion from internal models to various output formats.
8
+
9
+ autoload :JsonldTransformer, 'openphar/transformers/jsonld_transformer'
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ VERSION = "0.1.0"
5
+ end