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
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Cli
5
+ # Command-line interface for Openphar.
6
+ #
7
+ # Provides CLI commands for parsing, exporting, and validating data.
8
+
9
+ autoload :Main, 'openphar/cli/main'
10
+ end
11
+ end
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Core
5
+ # Core utilities for Openphar
6
+ #
7
+ # Contains shared utility classes used throughout the library.
8
+
9
+ autoload :SlugGenerator, 'openphar/core/slug_generator'
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Errors
5
+ # Raised when configuration is invalid.
6
+ class ConfigurationError < Openphar::Error; end
7
+ end
8
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Errors
5
+ # Raised when an unknown export format is specified.
6
+ class UnknownFormatError < Openphar::Error; end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Errors
5
+ # Raised when an unknown publisher is referenced.
6
+ class UnknownPublisherError < Openphar::Error; end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openphar
4
+ module Errors
5
+ # Raised when an unknown monograph type is referenced.
6
+ class UnknownTypeError < Openphar::Error; end
7
+ end
8
+ 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