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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6f9405e27ae7acba11ede41978b5fcc3d698d278f28fe30a0b5b1c46e4ad24d2
|
|
4
|
+
data.tar.gz: cb691edefe8f0637738fbcee5d31cdd0e8a87ece844cfed873ad15ae1c9db6c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 24acfa86829a8c1007c4947ae515d2ac72946b76ad24a54dfdf8b7870b0f2e8c066c76f9359df268360563a26e7533d137b63dbaa8636c0c3a3225b4f4a7f214
|
|
7
|
+
data.tar.gz: 6726eda92596a64cfd3b4d2dfd21c6f9b59a9960ccef32daac68fc73ce9812a038ac749c851cc4a0a6cb6ec9d052f4c30a11353c7083a66077f2ccacee0e08c5
|
data/README.adoc
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
= Open Pharmacopoeia
|
|
2
|
+
:toc: macro
|
|
3
|
+
:toclevels: 3
|
|
4
|
+
:sectanchors:
|
|
5
|
+
:sectlinks:
|
|
6
|
+
|
|
7
|
+
image:https://img.shields.io/badge/ontology-OWL%202-blue[OWL 2 Ontology]
|
|
8
|
+
image:https://img.shields.io/badge/format-JSON--LD-green[JSON-LD Format]
|
|
9
|
+
image:https://img.shields.io/badge/status-development-orange[Development Status]
|
|
10
|
+
|
|
11
|
+
== Purpose
|
|
12
|
+
|
|
13
|
+
Open Pharmacopoeia provides a harmonized ontology for representing pharmacopoeia monographs from multiple publishers. The project enables cross-publisher comparison of quality standards by normalizing test methods and specifications.
|
|
14
|
+
|
|
15
|
+
This project integrates with link:data-herbapedia[https://github.com/lutaml/data-herbapedia] for botanical and medicine system data, while open-pharmacopoeia focuses on quality standards and test specifications.
|
|
16
|
+
|
|
17
|
+
== Architecture
|
|
18
|
+
|
|
19
|
+
.Open-Pharmacopoeia Architecture Layers
|
|
20
|
+
[source]
|
|
21
|
+
----
|
|
22
|
+
data-herbapedia: Plant → Part → Preparation → Medicine System Profiles
|
|
23
|
+
↓
|
|
24
|
+
open-pharmacopoeia: PharmacopoeiaMonograph
|
|
25
|
+
├── TestSpecification
|
|
26
|
+
├── Assay
|
|
27
|
+
└── SafetyInformation
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
=== Integration with data-herbapedia
|
|
31
|
+
|
|
32
|
+
[cols="1,2"]
|
|
33
|
+
|===
|
|
34
|
+
| Layer | Responsibility
|
|
35
|
+
|
|
36
|
+
| data-herbapedia | Botanical data (PlantSpecies, PlantPart), HerbalPreparation, Medicine System Profiles (TCM, Ayurveda, Western)
|
|
37
|
+
| open-pharmacopoeia | Quality standards (TestSpecification, Assay), Test methods (HarmonizedMethod), Publisher-specific data
|
|
38
|
+
|===
|
|
39
|
+
|
|
40
|
+
The key integration point is `op:referencesPreparation`, which links a PharmacopoeiaMonograph to a HerbalPreparation in data-herbapedia.
|
|
41
|
+
|
|
42
|
+
== Features
|
|
43
|
+
|
|
44
|
+
* **Harmonized Ontology** - Common vocabulary for representing pharmacopoeia monographs from any publisher
|
|
45
|
+
* **Cross-Publisher Comparison** - Harmonized test methods enable comparison across publishers
|
|
46
|
+
* **Crude Drug Support** - Extended model for herbal medicines with macroscopic/microscopic descriptions
|
|
47
|
+
* **data-herbapedia Integration** - Links to botanical and medicine system data
|
|
48
|
+
* **Multilingual Support** - Full internationalization with language-tagged strings
|
|
49
|
+
* **SHACL Validation** - Shapes for validating monograph data
|
|
50
|
+
|
|
51
|
+
== Installation
|
|
52
|
+
|
|
53
|
+
Add this line to your application's Gemfile:
|
|
54
|
+
|
|
55
|
+
[source,ruby]
|
|
56
|
+
----
|
|
57
|
+
gem 'open-pharmacopoeia', git: 'https://github.com/lutaml/open-pharmacopoeia'
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
And then execute:
|
|
61
|
+
|
|
62
|
+
[source,shell]
|
|
63
|
+
----
|
|
64
|
+
bundle install
|
|
65
|
+
----
|
|
66
|
+
|
|
67
|
+
== Usage
|
|
68
|
+
|
|
69
|
+
=== Loading a Monograph
|
|
70
|
+
|
|
71
|
+
[source,ruby]
|
|
72
|
+
----
|
|
73
|
+
require 'open_pharmacopoeia'
|
|
74
|
+
|
|
75
|
+
# Load from JSON-LD
|
|
76
|
+
monograph = OpenPharmacopoeia::CrudeDrugMonograph.from_json(File.read('data/jp/radix-ginseng.jsonld'))
|
|
77
|
+
|
|
78
|
+
# Access monograph properties
|
|
79
|
+
monograph.label("en") #=> "Ginseng Radix"
|
|
80
|
+
monograph.publisher.name #=> "Japan Pharmacopoeia"
|
|
81
|
+
monograph.assays.each do |assay|
|
|
82
|
+
puts "#{assay.assay_target}: #{assay.limit}"
|
|
83
|
+
end
|
|
84
|
+
----
|
|
85
|
+
|
|
86
|
+
=== Creating a New Monograph
|
|
87
|
+
|
|
88
|
+
[source,ruby]
|
|
89
|
+
----
|
|
90
|
+
require 'open_pharmacopoeia'
|
|
91
|
+
|
|
92
|
+
monograph = OpenPharmacopoeia::CrudeDrugMonograph.new(
|
|
93
|
+
monograph_id: "JP18-001",
|
|
94
|
+
publisher: OpenPharmacopoeia::Publisher.find("JP"),
|
|
95
|
+
pref_label: { "en" => "Ginseng Radix", "ja" => "人参" },
|
|
96
|
+
references_preparation: "https://www.herbapedia.org/entity/preparation/dried-ginseng-root"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Add test specification
|
|
100
|
+
monograph.test_specifications << OpenPharmacopoeia::PhysicalSpecification.new(
|
|
101
|
+
test_name: "Total Ash",
|
|
102
|
+
test_type: OpenPharmacopoeia::TestType.find("Physical"),
|
|
103
|
+
harmonized_method: OpenPharmacopoeia::HarmonizedMethod.find("TotalAsh"),
|
|
104
|
+
limit: OpenPharmacopoeia::Limit.new(
|
|
105
|
+
limit_type: OpenPharmacopoeia::LimitType.find("NotMoreThan"),
|
|
106
|
+
limit_value: 4.2,
|
|
107
|
+
limit_unit: OpenPharmacopoeia::Unit.find("percent")
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
----
|
|
111
|
+
|
|
112
|
+
== Ontology Structure
|
|
113
|
+
|
|
114
|
+
=== Core Classes
|
|
115
|
+
|
|
116
|
+
[cols="1,3"]
|
|
117
|
+
|===
|
|
118
|
+
| Class | Description
|
|
119
|
+
|
|
120
|
+
| `op:PharmacopoeiaMonograph` | Central class representing a pharmacopoeia monograph
|
|
121
|
+
| `op:CrudeDrugMonograph` | Extended monograph for herbal medicines
|
|
122
|
+
| `op:TestSpecification` | Generic test specification (value object)
|
|
123
|
+
| `op:AssaySpecification` | Quantitative analysis specification
|
|
124
|
+
| `op:PuritySpecification` | Impurity limit specification
|
|
125
|
+
| `op:HarmonizedMethod` | Harmonized test method for cross-publisher comparison
|
|
126
|
+
| `op:Publisher` | Pharmacopoeia publisher enumeration
|
|
127
|
+
|===
|
|
128
|
+
|
|
129
|
+
=== External References
|
|
130
|
+
|
|
131
|
+
Monographs reference external entities in data-herbapedia:
|
|
132
|
+
|
|
133
|
+
[source,turtle]
|
|
134
|
+
----
|
|
135
|
+
op:Ginseng-Monograph
|
|
136
|
+
op:referencesPreparation <https://www.herbapedia.org/entity/preparation/dried-ginseng-root> ;
|
|
137
|
+
op:referencesTCMProfile <https://www.herbapedia.org/system/tcm/profile/ren-shen> .
|
|
138
|
+
----
|
|
139
|
+
|
|
140
|
+
=== Test Method Harmonization
|
|
141
|
+
|
|
142
|
+
[source,turtle]
|
|
143
|
+
----
|
|
144
|
+
# Harmonized method with publisher equivalents
|
|
145
|
+
method:HPLC a op:HarmonizedMethod ;
|
|
146
|
+
rdfs:label "HPLC"@en ;
|
|
147
|
+
op:jpReference "as directed under Liquid Chromatography <2.01>" ;
|
|
148
|
+
op:equivalentMethod api:Appendix-2.2.29 ;
|
|
149
|
+
op:equivalentMethod hkcmms:Appendix-IV-B .
|
|
150
|
+
----
|
|
151
|
+
|
|
152
|
+
== File Structure
|
|
153
|
+
|
|
154
|
+
[source]
|
|
155
|
+
----
|
|
156
|
+
open-pharmacopoeia/
|
|
157
|
+
├── ontology/
|
|
158
|
+
│ ├── core/
|
|
159
|
+
│ │ ├── pharmacopoeia.ttl # Core classes and properties
|
|
160
|
+
│ │ └── unit.ttl # Measurement units
|
|
161
|
+
│ ├── quality/
|
|
162
|
+
│ │ └── test-method.ttl # Harmonized test methods
|
|
163
|
+
│ └── context/
|
|
164
|
+
│ └── pharmacopoeia.jsonld # JSON-LD context
|
|
165
|
+
├── data/
|
|
166
|
+
│ ├── jp/ # Japan Pharmacopoeia monographs
|
|
167
|
+
│ ├── hkcmms/ # Hong Kong CMMS monographs
|
|
168
|
+
│ └── api/ # Ayurvedic Pharmacopoeia of India
|
|
169
|
+
├── shapes/
|
|
170
|
+
│ └── monograph-shapes.ttl # SHACL validation shapes
|
|
171
|
+
├── tools/
|
|
172
|
+
│ └── lib/
|
|
173
|
+
│ └── open_pharmacopoeia/ # Ruby models
|
|
174
|
+
└── reference-docs/ # Reference pharmacopoeia documents
|
|
175
|
+
----
|
|
176
|
+
|
|
177
|
+
== Supported Publishers
|
|
178
|
+
|
|
179
|
+
[cols="1,2,1"]
|
|
180
|
+
|===
|
|
181
|
+
| Code | Name | Country
|
|
182
|
+
|
|
183
|
+
| AHP | American Herbal Pharmacopoeia | US
|
|
184
|
+
| API | Ayurvedic Pharmacopoeia of India | IN
|
|
185
|
+
| JP | Japan Pharmacopoeia | JP
|
|
186
|
+
| HKCMMS | Hong Kong Chinese Materia Medica Standards | HK
|
|
187
|
+
| THP | Thai Herbal Pharmacopoeia | TH
|
|
188
|
+
| CMHerbarium | CM Herbarium | CN
|
|
189
|
+
| WHO | WHO Monographs on Selected Medicinal Plants | INT
|
|
190
|
+
| UPB | Unani Pharmacopoeia of Bangladesh | BD
|
|
191
|
+
| UPI | Unani Pharmacopoeia of India | IN
|
|
192
|
+
| APB | Ayurvedic Pharmacopoeia of Bangladesh | BD
|
|
193
|
+
| APS | Ayurvedic Pharmacopoeia of Sri Lanka | LK
|
|
194
|
+
|===
|
|
195
|
+
|
|
196
|
+
== IRI Namespaces
|
|
197
|
+
|
|
198
|
+
[cols="1,2"]
|
|
199
|
+
|===
|
|
200
|
+
| Prefix | Namespace
|
|
201
|
+
|
|
202
|
+
| `op:` | `https://www.openphar.org/ontology/core/`
|
|
203
|
+
| `method:` | `https://www.openphar.org/ontology/method/`
|
|
204
|
+
| `unit:` | `https://www.openphar.org/ontology/core/unit/`
|
|
205
|
+
| `herbapedia:` | `https://www.herbapedia.org/entity/`
|
|
206
|
+
|===
|
|
207
|
+
|
|
208
|
+
== Development
|
|
209
|
+
|
|
210
|
+
After checking out the repo, run:
|
|
211
|
+
|
|
212
|
+
[source,shell]
|
|
213
|
+
----
|
|
214
|
+
bundle install
|
|
215
|
+
bundle exec rake spec # Run tests
|
|
216
|
+
bundle exec rubocop # Run linter
|
|
217
|
+
----
|
|
218
|
+
|
|
219
|
+
== License
|
|
220
|
+
|
|
221
|
+
This project is available as open source under the terms of the MIT License.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Cli
|
|
5
|
+
# Main CLI for the openphar gem
|
|
6
|
+
class Main < Thor
|
|
7
|
+
package_name "openphar"
|
|
8
|
+
|
|
9
|
+
desc "version", "Show the openphar version"
|
|
10
|
+
def version
|
|
11
|
+
puts "openphar #{Openphar::VERSION}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "validate FILE", "Validate a JSON-LD monograph file"
|
|
15
|
+
option :format, aliases: "-f", default: "jsonld", desc: "Input format (jsonld, ttl)"
|
|
16
|
+
def validate(file)
|
|
17
|
+
puts "Validating #{file}..."
|
|
18
|
+
# TODO: Implement SHACL validation
|
|
19
|
+
puts "Validation complete"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "transform INPUT OUTPUT", "Transform monograph data to JSON-LD"
|
|
23
|
+
option :publisher, aliases: "-p", required: true, desc: "Publisher code (JP, PhInt, API, etc.)"
|
|
24
|
+
option :edition, aliases: "-e", desc: "Edition identifier (jp18, phint13, etc.)"
|
|
25
|
+
option :link, type: :boolean, default: false, desc: "Link to data-herbapedia"
|
|
26
|
+
def transform(input, output)
|
|
27
|
+
puts "Transforming #{input} to #{output}..."
|
|
28
|
+
puts "Publisher: #{options[:publisher]}"
|
|
29
|
+
puts "Edition: #{options[:edition]}" if options[:edition]
|
|
30
|
+
|
|
31
|
+
# Read input
|
|
32
|
+
content = File.read(input)
|
|
33
|
+
|
|
34
|
+
# Parse based on publisher
|
|
35
|
+
parser = parser_for(options[:publisher], content)
|
|
36
|
+
monographs = parser.parse
|
|
37
|
+
|
|
38
|
+
puts "Found #{monographs.length} monographs"
|
|
39
|
+
|
|
40
|
+
# Transform to JSON-LD
|
|
41
|
+
transformer = Transformers::JsonldTransformer.new
|
|
42
|
+
result = transformer.transform_graph(
|
|
43
|
+
monographs,
|
|
44
|
+
publisher: options[:publisher],
|
|
45
|
+
edition: options[:edition]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Write output
|
|
49
|
+
File.write(output, JSON.pretty_generate(result))
|
|
50
|
+
puts "Output written to #{output}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "link FILE", "Link monographs to data-herbapedia entities"
|
|
54
|
+
option :herbapedia, aliases: "-H", desc: "Path to data-herbapedia directory"
|
|
55
|
+
def link(file)
|
|
56
|
+
puts "Linking #{file} to data-herbapedia..."
|
|
57
|
+
|
|
58
|
+
linker = Linkers::HerbapediaLinker.new(
|
|
59
|
+
herbapedia_data_path: options[:herbapedia]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Read and parse JSON-LD
|
|
63
|
+
data = JSON.parse(File.read(file))
|
|
64
|
+
|
|
65
|
+
# Link each monograph
|
|
66
|
+
if data["@graph"]
|
|
67
|
+
data["@graph"].map! do |monograph|
|
|
68
|
+
linker.link_monograph(monograph)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
data = linker.link_monograph(data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Write updated file
|
|
75
|
+
File.write(file, JSON.pretty_generate(data))
|
|
76
|
+
puts "Links added to #{file}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "publishers", "List all supported pharmacopoeia publishers"
|
|
80
|
+
def publishers
|
|
81
|
+
puts "Supported Publishers:"
|
|
82
|
+
puts "-" * 60
|
|
83
|
+
Publisher.all.each do |publisher|
|
|
84
|
+
puts format("%-10s %s (%s)",
|
|
85
|
+
publisher.code,
|
|
86
|
+
publisher.name["en"],
|
|
87
|
+
publisher.country)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc "editions", "List available pharmacopoeia editions"
|
|
92
|
+
def editions
|
|
93
|
+
edition_dir = File.join(Openphar.data_path, "edition")
|
|
94
|
+
return puts "No editions found" unless File.directory?(edition_dir)
|
|
95
|
+
|
|
96
|
+
puts "Available Editions:"
|
|
97
|
+
puts "-" * 60
|
|
98
|
+
|
|
99
|
+
Dir.glob("#{edition_dir}/*/*.jsonld").each do |file|
|
|
100
|
+
data = JSON.parse(File.read(file))
|
|
101
|
+
next unless data["@graph"]
|
|
102
|
+
|
|
103
|
+
data["@graph"].each do |item|
|
|
104
|
+
next unless item["@type"] == "Edition"
|
|
105
|
+
|
|
106
|
+
label = item["prefLabel"]&.values&.first || item["editionNumber"]
|
|
107
|
+
count = item["monographCount"] || "unknown"
|
|
108
|
+
puts format("%-20s %s monographs", label, count)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
desc "compare MONOGRAPH_ID", "Compare a monograph across publishers"
|
|
114
|
+
option :format, aliases: "-f", default: "table", desc: "Output format (table, json)"
|
|
115
|
+
def compare(monograph_id)
|
|
116
|
+
puts "Comparing #{monograph_id} across publishers..."
|
|
117
|
+
puts "Not implemented yet"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def parser_for(publisher, content)
|
|
123
|
+
case publisher.upcase
|
|
124
|
+
when "JP"
|
|
125
|
+
Parsers::JpHtmlParser.new(content)
|
|
126
|
+
else
|
|
127
|
+
raise Error, "Unknown publisher: #{publisher}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Register as a binary
|
|
135
|
+
if __FILE__ == $PROGRAM_NAME
|
|
136
|
+
Openphar::Cli::Main.start(ARGV)
|
|
137
|
+
end
|
data/lib/openphar/cli.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Core
|
|
5
|
+
# Unified slug generator for consistent URL-safe identifier generation.
|
|
6
|
+
#
|
|
7
|
+
# Consolidates multiple slug generation implementations into a single class.
|
|
8
|
+
# All slugs are lowercase, hyphen-separated, and contain only alphanumeric
|
|
9
|
+
# characters and hyphens.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# SlugGenerator.generate("Zingiberis Rhizoma")
|
|
13
|
+
# # => "zingiberis-rhizoma"
|
|
14
|
+
#
|
|
15
|
+
# @example With fallback
|
|
16
|
+
# SlugGenerator.generate(nil)
|
|
17
|
+
# # => "unknown"
|
|
18
|
+
#
|
|
19
|
+
# @example Latin name handling
|
|
20
|
+
# SlugGenerator.generate_latin("Acacia (Gum Arabic)")
|
|
21
|
+
# # => "acacia-gum-arabic"
|
|
22
|
+
class SlugGenerator
|
|
23
|
+
# Default slug for nil/empty inputs
|
|
24
|
+
DEFAULT_SLUG = 'unknown'
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Generate a URL-safe slug from any name.
|
|
28
|
+
#
|
|
29
|
+
# @param name [String, nil] The name to convert to a slug
|
|
30
|
+
# @return [String] A URL-safe slug
|
|
31
|
+
def generate(name)
|
|
32
|
+
return DEFAULT_SLUG unless name
|
|
33
|
+
|
|
34
|
+
result = name.to_s.downcase
|
|
35
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
|
36
|
+
.strip
|
|
37
|
+
.gsub(/\s+/, '-')
|
|
38
|
+
.gsub(/-+/, '-')
|
|
39
|
+
.sub(/^-/, '')
|
|
40
|
+
.sub(/-$/, '')
|
|
41
|
+
|
|
42
|
+
result.empty? ? DEFAULT_SLUG : result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate a slug optimized for Latin pharmaceutical names.
|
|
46
|
+
#
|
|
47
|
+
# Latin names often have specific formatting like:
|
|
48
|
+
# - "Zingiberis Rhizoma" (binomial form)
|
|
49
|
+
# - "Acacia (Gum Arabic)" (with common name)
|
|
50
|
+
#
|
|
51
|
+
# @param latin_name [String, nil] The Latin name to convert
|
|
52
|
+
# @return [String] A URL-safe slug
|
|
53
|
+
def generate_latin(latin_name)
|
|
54
|
+
return DEFAULT_SLUG unless latin_name
|
|
55
|
+
|
|
56
|
+
# Remove content in parentheses (common names)
|
|
57
|
+
cleaned = latin_name.to_s.gsub(/\s*\([^)]*\)\s*/, ' ').strip
|
|
58
|
+
|
|
59
|
+
result = cleaned.downcase
|
|
60
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
|
61
|
+
.strip
|
|
62
|
+
.gsub(/\s+/, '-')
|
|
63
|
+
.gsub(/-+/, '-')
|
|
64
|
+
.sub(/^-/, '')
|
|
65
|
+
.sub(/-$/, '')
|
|
66
|
+
|
|
67
|
+
result.empty? ? DEFAULT_SLUG : result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generate a slug from a title with potential English and Latin parts.
|
|
71
|
+
#
|
|
72
|
+
# Titles often come in formats like:
|
|
73
|
+
# - "Abacavir sulfate (Abacaviri sulfas)"
|
|
74
|
+
# - "Ginger Rhizome"
|
|
75
|
+
#
|
|
76
|
+
# @param title [String, nil] The title to convert
|
|
77
|
+
# @return [String] A URL-safe slug
|
|
78
|
+
def generate_from_title(title)
|
|
79
|
+
return DEFAULT_SLUG unless title
|
|
80
|
+
|
|
81
|
+
# Extract English name (before parentheses if present)
|
|
82
|
+
english_part = title.to_s.split(/\s*\(/).first.strip
|
|
83
|
+
|
|
84
|
+
generate(english_part)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Generate a unique slug, appending a number if necessary.
|
|
88
|
+
#
|
|
89
|
+
# @param name [String, nil] The base name
|
|
90
|
+
# @param existing_slugs [Set<String>, Array<String>] Slugs that already exist
|
|
91
|
+
# @return [String] A unique slug
|
|
92
|
+
def generate_unique(name, existing_slugs)
|
|
93
|
+
base_slug = generate(name)
|
|
94
|
+
return base_slug unless existing_slugs.include?(base_slug)
|
|
95
|
+
|
|
96
|
+
counter = 1
|
|
97
|
+
loop do
|
|
98
|
+
candidate = "#{base_slug}-#{counter}"
|
|
99
|
+
return candidate unless existing_slugs.include?(candidate)
|
|
100
|
+
|
|
101
|
+
counter += 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when a monograph cannot be found.
|
|
6
|
+
class MonographNotFoundError < Openphar::Error
|
|
7
|
+
attr_reader :monograph_id
|
|
8
|
+
|
|
9
|
+
def initialize(monograph_id)
|
|
10
|
+
@monograph_id = monograph_id
|
|
11
|
+
super("Monograph not found: #{monograph_id}")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when parsing fails.
|
|
6
|
+
class ParseError < Openphar::Error
|
|
7
|
+
attr_reader :source, :details
|
|
8
|
+
|
|
9
|
+
def initialize(source, details = nil)
|
|
10
|
+
@source = source
|
|
11
|
+
@details = details
|
|
12
|
+
message = "Failed to parse: #{source}"
|
|
13
|
+
message += " - #{details}" if details
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Errors
|
|
5
|
+
# Raised when validation fails.
|
|
6
|
+
class ValidationError < Openphar::Error
|
|
7
|
+
attr_reader :errors
|
|
8
|
+
|
|
9
|
+
def initialize(errors = [])
|
|
10
|
+
@errors = errors
|
|
11
|
+
super("Validation failed: #{errors.join(', ')}")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Openphar
|
|
4
|
+
module Errors
|
|
5
|
+
# Error classes for Openphar.
|
|
6
|
+
#
|
|
7
|
+
# Provides specific exception types for different error conditions.
|
|
8
|
+
|
|
9
|
+
autoload :UnknownPublisherError, 'openphar/errors/unknown_publisher_error'
|
|
10
|
+
autoload :UnknownTypeError, 'openphar/errors/unknown_type_error'
|
|
11
|
+
autoload :UnknownFormatError, 'openphar/errors/unknown_format_error'
|
|
12
|
+
autoload :MonographNotFoundError, 'openphar/errors/monograph_not_found_error'
|
|
13
|
+
autoload :ParseError, 'openphar/errors/parse_error'
|
|
14
|
+
autoload :ValidationError, 'openphar/errors/validation_error'
|
|
15
|
+
autoload :ConfigurationError, 'openphar/errors/configuration_error'
|
|
16
|
+
end
|
|
17
|
+
end
|