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.
- checksums.yaml +7 -0
- data/README.adoc +221 -0
- data/lib/openphar/cli/main.rb +137 -0
- data/lib/openphar/cli.rb +11 -0
- data/lib/openphar/core/slug_generator.rb +107 -0
- data/lib/openphar/core.rb +11 -0
- data/lib/openphar/errors/configuration_error.rb +8 -0
- data/lib/openphar/errors/monograph_not_found_error.rb +15 -0
- data/lib/openphar/errors/parse_error.rb +18 -0
- data/lib/openphar/errors/unknown_format_error.rb +8 -0
- data/lib/openphar/errors/unknown_publisher_error.rb +8 -0
- data/lib/openphar/errors/unknown_type_error.rb +8 -0
- data/lib/openphar/errors/validation_error.rb +15 -0
- data/lib/openphar/errors.rb +17 -0
- data/lib/openphar/exporters/coverage_validator.rb +265 -0
- data/lib/openphar/exporters/neo4j/model_registry.rb +130 -0
- data/lib/openphar/exporters/neo4j/node_builder.rb +84 -0
- data/lib/openphar/exporters/neo4j/property_mapper.rb +154 -0
- data/lib/openphar/exporters/neo4j/relationship_builder.rb +87 -0
- data/lib/openphar/exporters/neo4j.rb +16 -0
- data/lib/openphar/exporters/neo4j_exporter.rb +199 -0
- data/lib/openphar/exporters.rb +15 -0
- data/lib/openphar/linkers/chp/cross_edition_linker.rb +101 -0
- data/lib/openphar/linkers/chp.rb +11 -0
- data/lib/openphar/linkers/cross_publisher_linker.rb +295 -0
- data/lib/openphar/linkers/herbapedia_linker.rb +125 -0
- data/lib/openphar/linkers.rb +13 -0
- data/lib/openphar/migrators/chp/classifier.rb +86 -0
- data/lib/openphar/migrators/chp.rb +15 -0
- data/lib/openphar/migrators/chp_migrator.rb +130 -0
- data/lib/openphar/migrators/jp_migrator.rb +345 -0
- data/lib/openphar/migrators/phint_migrator.rb +426 -0
- data/lib/openphar/migrators.rb +15 -0
- data/lib/openphar/models/ahp.rb +25 -0
- data/lib/openphar/models/amino_acid_monograph.rb +28 -0
- data/lib/openphar/models/api.rb +29 -0
- data/lib/openphar/models/assay_specification.rb +19 -0
- data/lib/openphar/models/base_entity.rb +221 -0
- data/lib/openphar/models/biological_substance_monograph.rb +31 -0
- data/lib/openphar/models/chemical_drug_monograph.rb +39 -0
- data/lib/openphar/models/chp/biologic.rb +28 -0
- data/lib/openphar/models/chp/chemical_preparation.rb +28 -0
- data/lib/openphar/models/chp/chemical_substance.rb +28 -0
- data/lib/openphar/models/chp/general_chapter.rb +30 -0
- data/lib/openphar/models/chp/monograph.rb +116 -0
- data/lib/openphar/models/chp/section.rb +37 -0
- data/lib/openphar/models/chp/tcm_crude_drug.rb +28 -0
- data/lib/openphar/models/chp/tcm_extract.rb +29 -0
- data/lib/openphar/models/chp/tcm_formulation.rb +28 -0
- data/lib/openphar/models/chp.rb +33 -0
- data/lib/openphar/models/crude_drug_monograph.rb +31 -0
- data/lib/openphar/models/edition.rb +41 -0
- data/lib/openphar/models/formulation_monograph.rb +31 -0
- data/lib/openphar/models/hkcmms.rb +26 -0
- data/lib/openphar/models/identification_specification.rb +19 -0
- data/lib/openphar/models/jp/kampo_formula.rb +94 -0
- data/lib/openphar/models/jp.rb +21 -0
- data/lib/openphar/models/limit.rb +54 -0
- data/lib/openphar/models/mineral_substance_monograph.rb +28 -0
- data/lib/openphar/models/monograph.rb +58 -0
- data/lib/openphar/models/ph_int/buffer_solution.rb +38 -0
- data/lib/openphar/models/ph_int/dosage_form_monograph.rb +43 -0
- data/lib/openphar/models/ph_int/radiopharmaceutical_monograph.rb +52 -0
- data/lib/openphar/models/ph_int/reagent.rb +50 -0
- data/lib/openphar/models/ph_int/reference_substance.rb +50 -0
- data/lib/openphar/models/ph_int/test_method.rb +50 -0
- data/lib/openphar/models/ph_int/test_solution.rb +39 -0
- data/lib/openphar/models/ph_int/volumetric_solution.rb +38 -0
- data/lib/openphar/models/ph_int.rb +37 -0
- data/lib/openphar/models/physical_specification.rb +17 -0
- data/lib/openphar/models/publisher.rb +98 -0
- data/lib/openphar/models/purity_specification.rb +16 -0
- data/lib/openphar/models/supplement.rb +31 -0
- data/lib/openphar/models/test_specification.rb +29 -0
- data/lib/openphar/models/thp.rb +24 -0
- data/lib/openphar/models/vitamin_monograph.rb +28 -0
- data/lib/openphar/models.rb +58 -0
- data/lib/openphar/monograph_merger.rb +374 -0
- data/lib/openphar/parsers/base_monograph_parser.rb +108 -0
- data/lib/openphar/parsers/jp_html_parser.rb +92 -0
- data/lib/openphar/parsers/jp_html_parser_base.rb +113 -0
- data/lib/openphar/parsers/jp_ja_html_parser.rb +220 -0
- data/lib/openphar/parsers/phint_json_parser.rb +332 -0
- data/lib/openphar/parsers.rb +21 -0
- data/lib/openphar/registry/publisher_registry.rb +46 -0
- data/lib/openphar/registry/type_registry.rb +147 -0
- data/lib/openphar/registry.rb +24 -0
- data/lib/openphar/repositories/monograph_repository.rb +350 -0
- data/lib/openphar/repositories.rb +11 -0
- data/lib/openphar/transformers/jsonld_transformer.rb +106 -0
- data/lib/openphar/transformers.rb +11 -0
- data/lib/openphar/version.rb +5 -0
- data/lib/openphar.rb +119 -0
- data/ontology/bibliographic/edition.ttl +267 -0
- data/ontology/context/chp.jsonld +69 -0
- data/ontology/context/crude-drugs.jsonld +56 -0
- data/ontology/context/pharmacopoeia.jsonld +631 -0
- data/ontology/core/identification.ttl +456 -0
- data/ontology/core/pharmacopoeia.ttl +3427 -0
- data/ontology/core/substance-form.ttl +302 -0
- data/ontology/core/unit.ttl +493 -0
- data/ontology/publisher/chp/extensions.ttl +186 -0
- data/ontology/publisher/phint/extensions.ttl +327 -0
- data/ontology/quality/test-method.ttl +610 -0
- data/shapes/chp/monograph.ttl +153 -0
- data/shapes/edition-shapes.ttl +230 -0
- data/shapes/monograph-shapes.ttl +1324 -0
- 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
|