citedhealth 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d1412f03c7f37f1c6e05d1cf651bd2185e9fe0a3697793bb352387199f907acf
4
+ data.tar.gz: 3237403eebbb4a5710df421e9462e43353a9c9eb8f772f1b99617e08695aee36
5
+ SHA512:
6
+ metadata.gz: 7fa44b8d9936beb53096330f88563dfcde9a17b4d45e8383da02ba4bd0e6ee937fbcade18f5eeae66d2989f7d9f7baec811c2850d7b88062265593648045ea1c
7
+ data.tar.gz: ded37c88e38fe195e3efe64a7e6383dbd10105621b1d6c68e8e915a41cfed52288caf78bd9441863e1e6526c001287a74aeaa4dc50284b69b1b215757ba8fc3c
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module CitedHealth
8
+ # HTTP client for the Cited Health REST API.
9
+ #
10
+ # All methods return typed Ruby objects (Ingredient, Paper, EvidenceLink).
11
+ # Zero runtime dependencies — uses only Ruby stdlib (net/http, json, uri).
12
+ #
13
+ # client = CitedHealth::Client.new
14
+ # ingredient = client.get_ingredient("vitamin-d")
15
+ # puts ingredient.name # => "Vitamin D"
16
+ #
17
+ class Client
18
+ DEFAULT_BASE_URL = "https://citedhealth.com"
19
+ DEFAULT_TIMEOUT = 30
20
+
21
+ def initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
22
+ @base_url = base_url.chomp("/")
23
+ @timeout = timeout
24
+ end
25
+
26
+ # Search ingredients by query and/or category.
27
+ #
28
+ # @param query [String] search term (default: "")
29
+ # @param category [String] filter by category (default: "")
30
+ # @return [Array<Ingredient>] list of matching ingredients
31
+ def search_ingredients(query: "", category: "")
32
+ params = {}
33
+ params[:q] = query unless query.empty?
34
+ params[:category] = category unless category.empty?
35
+
36
+ data = get("/api/ingredients/", params)
37
+ results = data["results"] || []
38
+ results.map { |h| Ingredient.from_hash(h) }
39
+ end
40
+
41
+ # Get a single ingredient by slug.
42
+ #
43
+ # @param slug [String] ingredient slug (e.g. "vitamin-d")
44
+ # @return [Ingredient]
45
+ # @raise [NotFoundError] if the ingredient does not exist
46
+ def get_ingredient(slug)
47
+ data = get("/api/ingredients/#{slug}/")
48
+ Ingredient.from_hash(data)
49
+ end
50
+
51
+ # Get evidence linking an ingredient to a condition.
52
+ #
53
+ # @param ingredient_slug [String] ingredient slug
54
+ # @param condition_slug [String] condition slug
55
+ # @return [EvidenceLink] the first matching evidence link
56
+ # @raise [NotFoundError] if no evidence exists for the pair
57
+ def get_evidence(ingredient_slug:, condition_slug:)
58
+ data = get("/api/evidence/", ingredient: ingredient_slug, condition: condition_slug)
59
+ results = data["results"] || []
60
+ raise NotFoundError, "No evidence found: #{ingredient_slug} × #{condition_slug}" if results.empty?
61
+
62
+ EvidenceLink.from_hash(results.first)
63
+ end
64
+
65
+ # Get a single evidence link by ID.
66
+ #
67
+ # @param id [Integer] evidence link ID
68
+ # @return [EvidenceLink]
69
+ # @raise [NotFoundError] if the evidence link does not exist
70
+ def get_evidence_by_id(id)
71
+ data = get("/api/evidence/#{id}/")
72
+ EvidenceLink.from_hash(data)
73
+ end
74
+
75
+ # Search research papers by query and/or publication year.
76
+ #
77
+ # @param query [String] search term (default: "")
78
+ # @param year [Integer, nil] filter by publication year
79
+ # @return [Array<Paper>] list of matching papers
80
+ def search_papers(query: "", year: nil)
81
+ params = {}
82
+ params[:q] = query unless query.empty?
83
+ params[:year] = year.to_s unless year.nil?
84
+
85
+ data = get("/api/papers/", params)
86
+ results = data["results"] || []
87
+ results.map { |h| Paper.from_hash(h) }
88
+ end
89
+
90
+ # Get a single paper by PubMed ID.
91
+ #
92
+ # @param pmid [String] PubMed ID
93
+ # @return [Paper]
94
+ # @raise [NotFoundError] if the paper does not exist
95
+ def get_paper(pmid)
96
+ data = get("/api/papers/#{pmid}/")
97
+ Paper.from_hash(data)
98
+ end
99
+
100
+ private
101
+
102
+ def get(path, params = {})
103
+ uri = URI("#{@base_url}#{path}")
104
+ uri.query = URI.encode_www_form(params) unless params.empty?
105
+
106
+ http = Net::HTTP.new(uri.host, uri.port)
107
+ http.use_ssl = uri.scheme == "https"
108
+ http.open_timeout = @timeout
109
+ http.read_timeout = @timeout
110
+
111
+ request = Net::HTTP::Get.new(uri)
112
+ request["Accept"] = "application/json"
113
+ request["User-Agent"] = "citedhealth-rb/#{VERSION}"
114
+
115
+ response = http.request(request)
116
+
117
+ case response
118
+ when Net::HTTPSuccess
119
+ JSON.parse(response.body)
120
+ when Net::HTTPNotFound
121
+ raise NotFoundError, "Not found: #{path}"
122
+ when Net::HTTPTooManyRequests
123
+ retry_after = response["Retry-After"]&.to_i
124
+ raise RateLimitError.new("Rate limit exceeded", retry_after: retry_after)
125
+ else
126
+ raise Error, "HTTP #{response.code}: #{response.body}"
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CitedHealth
4
+ # General API error.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the server returns HTTP 404.
8
+ class NotFoundError < Error; end
9
+
10
+ # Raised when the server returns HTTP 429.
11
+ # Exposes the Retry-After header value when present.
12
+ class RateLimitError < Error
13
+ attr_reader :retry_after
14
+
15
+ def initialize(message = "Rate limit exceeded", retry_after: nil)
16
+ @retry_after = retry_after
17
+ super(message)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CitedHealth
4
+ # A health ingredient with dosage and form information.
5
+ class Ingredient
6
+ attr_reader :id, :name, :slug, :category, :mechanism,
7
+ :recommended_dosage, :forms, :is_featured
8
+
9
+ def initialize(id:, name:, slug:, category: "", mechanism: "",
10
+ recommended_dosage: {}, forms: [], is_featured: false)
11
+ @id = id
12
+ @name = name
13
+ @slug = slug
14
+ @category = category
15
+ @mechanism = mechanism
16
+ @recommended_dosage = recommended_dosage
17
+ @forms = forms
18
+ @is_featured = is_featured
19
+ end
20
+
21
+ def self.from_hash(hash)
22
+ new(
23
+ id: hash["id"],
24
+ name: hash["name"],
25
+ slug: hash["slug"],
26
+ category: hash["category"] || "",
27
+ mechanism: hash["mechanism"] || "",
28
+ recommended_dosage: hash["recommended_dosage"] || {},
29
+ forms: hash["forms"] || [],
30
+ is_featured: hash["is_featured"] || false
31
+ )
32
+ end
33
+ end
34
+
35
+ # A health condition referenced in evidence links.
36
+ class Condition
37
+ attr_reader :slug, :name
38
+
39
+ def initialize(slug:, name:)
40
+ @slug = slug
41
+ @name = name
42
+ end
43
+
44
+ def self.from_hash(hash)
45
+ new(
46
+ slug: hash["slug"],
47
+ name: hash["name"]
48
+ )
49
+ end
50
+ end
51
+
52
+ # A research paper from PubMed.
53
+ class Paper
54
+ attr_reader :id, :pmid, :title, :journal, :publication_year,
55
+ :study_type, :citation_count, :is_open_access, :pubmed_link
56
+
57
+ def initialize(id:, pmid:, title:, journal: "", publication_year: nil,
58
+ study_type: "", citation_count: 0, is_open_access: false,
59
+ pubmed_link: "")
60
+ @id = id
61
+ @pmid = pmid
62
+ @title = title
63
+ @journal = journal
64
+ @publication_year = publication_year
65
+ @study_type = study_type
66
+ @citation_count = citation_count
67
+ @is_open_access = is_open_access
68
+ @pubmed_link = pubmed_link
69
+ end
70
+
71
+ def self.from_hash(hash)
72
+ new(
73
+ id: hash["id"],
74
+ pmid: hash["pmid"],
75
+ title: hash["title"],
76
+ journal: hash["journal"] || "",
77
+ publication_year: hash["publication_year"],
78
+ study_type: hash["study_type"] || "",
79
+ citation_count: hash["citation_count"] || 0,
80
+ is_open_access: hash["is_open_access"] || false,
81
+ pubmed_link: hash["pubmed_link"] || ""
82
+ )
83
+ end
84
+ end
85
+
86
+ # Nested ingredient reference within an evidence link.
87
+ class NestedIngredient
88
+ attr_reader :slug, :name
89
+
90
+ def initialize(slug:, name:)
91
+ @slug = slug
92
+ @name = name
93
+ end
94
+
95
+ def self.from_hash(hash)
96
+ new(
97
+ slug: hash["slug"],
98
+ name: hash["name"]
99
+ )
100
+ end
101
+ end
102
+
103
+ # An evidence link connecting an ingredient to a condition with a grade.
104
+ class EvidenceLink
105
+ attr_reader :id, :ingredient, :condition, :grade, :grade_label,
106
+ :summary, :direction, :total_studies, :total_participants
107
+
108
+ def initialize(id:, ingredient:, condition:, grade: "", grade_label: "",
109
+ summary: "", direction: "", total_studies: 0,
110
+ total_participants: 0)
111
+ @id = id
112
+ @ingredient = ingredient
113
+ @condition = condition
114
+ @grade = grade
115
+ @grade_label = grade_label
116
+ @summary = summary
117
+ @direction = direction
118
+ @total_studies = total_studies
119
+ @total_participants = total_participants
120
+ end
121
+
122
+ def self.from_hash(hash)
123
+ ingredient_data = hash["ingredient"] || {}
124
+ condition_data = hash["condition"] || {}
125
+
126
+ new(
127
+ id: hash["id"],
128
+ ingredient: NestedIngredient.from_hash(ingredient_data),
129
+ condition: Condition.from_hash(condition_data),
130
+ grade: hash["grade"] || "",
131
+ grade_label: hash["grade_label"] || "",
132
+ summary: hash["summary"] || "",
133
+ direction: hash["direction"] || "",
134
+ total_studies: hash["total_studies"] || 0,
135
+ total_participants: hash["total_participants"] || 0
136
+ )
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CitedHealth
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "citedhealth/version"
4
+ require_relative "citedhealth/errors"
5
+ require_relative "citedhealth/types"
6
+ require_relative "citedhealth/client"
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: citedhealth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Cited Health
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: webmock
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ description: API client for citedhealth.com. Search ingredients, evidence links, and
55
+ research papers for evidence-based health supplement information. Zero dependencies.
56
+ email:
57
+ - hello@citedhealth.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - lib/citedhealth.rb
63
+ - lib/citedhealth/client.rb
64
+ - lib/citedhealth/errors.rb
65
+ - lib/citedhealth/types.rb
66
+ - lib/citedhealth/version.rb
67
+ homepage: https://citedhealth.com
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://citedhealth.com
72
+ source_code_uri: https://github.com/citedhealth/citedhealth-rb
73
+ changelog_uri: https://github.com/citedhealth/citedhealth-rb/blob/main/CHANGELOG.md
74
+ documentation_uri: https://citedhealth.com/developers/
75
+ bug_tracker_uri: https://github.com/citedhealth/citedhealth-rb/issues
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3.0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 4.0.3
91
+ specification_version: 4
92
+ summary: Ruby client for the Cited Health evidence-based supplement API
93
+ test_files: []