guardian_searcher 0.1.2 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbf80793abdcb30ddb0acb6ff62cc97bf5d2e230881d2057e391f61c3490fa4e
4
- data.tar.gz: c6df4f2b8cd92f9ba2ae7fa29ae57eb3e4ad3d2a7637176e0c1a8ec53b7f654e
3
+ metadata.gz: e3df7ad5dba2bad5467f4f3b0b09dc360a7e8c2b097dfddbb6bbc126ba997af1
4
+ data.tar.gz: 700a5915369af7e4313b4e94601d5d68a3e83f164fd3e7f6ef702a26fae8f350
5
5
  SHA512:
6
- metadata.gz: 6d5779ce66ac3507594a07a52db236e1c581729e8b2b05d27ff11287ef8e9dce7eb69b54a9f80298a0fc2d096d3b952f1eb8fc64d3b389713fbedfd0a8e70720
7
- data.tar.gz: de83958f007a38e71bfe15d4637aa645feb2d88e370582cef5de3c4fc67a9daf720fae26bd4062e7a32ce7c6a5921aa42d8d5b57be8af8f51514ec2cd8e3fda9
6
+ metadata.gz: 3f53981c222f1409b0541b555f07b59dd2aa64847d6680255008ac739f40e2a66669d6dc3be1fe6d012c86bac6fe15aef691577510715a3f573b3a5453bc1d67
7
+ data.tar.gz: 295dfad1a4a45d31543c264f24a0c76c4e0354fc0b666293b9f810e9727e0b5ea7c5fc4e909e22f8de89ac5e4749fa6d8316e6999d8d4cc082b12b948a0447f5
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: Guardian Searcher gem test suite
3
+ run-name: ${{ github.actor }} is running the test suite
4
+ on:
5
+ push:
6
+ branches: [develop]
7
+ pull_request:
8
+ branches: [main]
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ ruby-version: ['2.7', '3.0', '3.1', '3.3']
16
+ steps:
17
+ - uses: actions/checkout@v3
18
+ - name: Install Ruby ${{ matrix.ruby-version }}
19
+ uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby-version }}
22
+ bundler-cache: true
23
+ - name: Install dependencies
24
+ run: bundle install
25
+ - name: Run tests
26
+ run: bundle exec rake spec
27
+
28
+
data/.gitignore CHANGED
@@ -11,4 +11,6 @@
11
11
  # rspec failure tracking
12
12
  Gemfile.lock
13
13
  .rspec_status
14
+ .byebug_history
14
15
  .DS_Store
16
+ project_context.txt
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
1
  ## [Unreleased]
2
+ - Parse results and return an exception based on the Guardian API response
3
+
4
+ ## [0.1.4] - 2026-06-28
5
+ - Moved API key from URL query string to request header to prevent leakage in logs
2
6
 
3
7
  ## [0.1.0] - 2022-10-01
4
8
 
@@ -7,3 +11,15 @@
7
11
  ## [0.1.1] - 2022-10-01
8
12
 
9
13
  - Fix dependency warnings
14
+
15
+ ## [0.1.2] - 2022-10-04
16
+
17
+ - Moved options parse in their Options class
18
+ - Updated readme
19
+
20
+ ## [0.1.3] - 2022-10-23
21
+
22
+ - Added Content class
23
+ - Added Helpers classes ( Generator & Util)
24
+ - Added some additional methods to Base class - search tags and editons endpoints
25
+ - Improved code coverage (Happy Paths only for now)
data/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # GuardianSearcher
2
2
 
3
- This is a work in progress, and its status is currently not even an alpha version. Tests needs to be implemented and the code is not optimal.
3
+ [![Gem Version](https://badge.fury.io/rb/guardian_searcher.svg)](https://badge.fury.io/rb/guardian_searcher)
4
+
5
+ This is a work in progress, and its status is currently an alpha version. Tests needs to be implemented and the code is not optimal.
4
6
  The goal of this project is to provide a Ruby wrapper to query the Guardian Api and to experiment with some programming techniques.
5
7
 
8
+ Documentation of TheGuardian API is [Here](https://open-platform.theguardian.com/documentation/)
9
+
6
10
  If you wanna try it you need to have an API key and use it as an environment variable.
7
11
 
8
12
  ```bash
@@ -57,6 +61,93 @@ results = searcher.search('your keyword', { from_date: '2022-10-01', page_size:
57
61
 
58
62
  If you add something unsupported it will throw an `OptionsNotSupportedError`
59
63
 
64
+ The results of the search can be used as they are, a Farady response object or you can parse - remember to check for the response code first - them using `GuardianSearcher::SearchResult` in the following way:
65
+
66
+ ```ruby
67
+ response_body = searcher.search('your keyword', { from_date: '2022-10-01', page_size: 10 }).body
68
+ results = GuardianSearcher::SearchResult.parse_results(body: response_body)
69
+ ```
70
+ This will return a `SearchResult` object which the following attributes:
71
+
72
+ ```ruby
73
+ @current_page
74
+ @results # an array with all the search results
75
+ @page_size # paging size
76
+ @pages # number of pages
77
+ @start # starting page
78
+ ```
79
+
80
+ However if you want the gem to take care of the response codes and use its built in errors just use
81
+
82
+ ```ruby
83
+ response = searcher.search('your keyword', { from_date: '2022-10-01', page_size: 10 })
84
+ results = GuardianSearcher::SearchResult.parse_with_codes(response)
85
+ ```
86
+
87
+ This will return a `SearchResult` object or one of the following errors
88
+
89
+ ```ruby
90
+ GuardianUnauthorizedError # when a 401 is returned
91
+ GuardianBadRequestError # when a 400 is returned
92
+ GuardianInternalServerError # when a 500 is returned
93
+ GuardianUnknownError # when an error code is not among the above ones
94
+ ```
95
+
96
+ Of interest the structure of a single element of the results array, which is an Hash array similar to this
97
+
98
+ ```ruby
99
+ {"id"=>"football/2022/sep/23/player-mutiny-exposes-deeper-issues-within-spanish-womens-football",
100
+ "type"=>"article",
101
+ "sectionId"=>"football",
102
+ "sectionName"=>"Football",
103
+ "webPublicationDate"=>"2022-09-23T19:20:09Z",
104
+ "webTitle"=>"Player mutiny exposes deeper issues within Spanish women’s football | Sid Lowe",
105
+ "webUrl"=>"https://www.theguardian.com/football/2022/sep/23/player-mutiny-exposes-deeper-issues-within-spanish-womens-football",
106
+ "apiUrl"=>"https://content.guardianapis.com/football/2022/sep/23/player-mutiny-exposes-deeper-issues-within-spanish-womens-football",
107
+ "isHosted"=>false,
108
+ "pillarId"=>"pillar/sport",
109
+ "pillarName"=>"Sport"}
110
+ ```
111
+ At this point you can use the `SearchResult` object as it is or you could convert it to an Array of `Content` objects in the following way:
112
+ ```ruby
113
+ generator = GuardianSearcher::Helpers::Generator.new
114
+ # results is the SearchResult object created before which has an attribute
115
+ # called results. Not a great name choice but sorry about that
116
+ contents = generator.generate(results.results, "GuardianSearcher::Content")
117
+ ```
118
+
119
+ Each element of the `contents` Array will be an instance of the `Content` class, with a number of attributes that depends on the returned results i.e. that
120
+ if an element of the results attribute is something like:
121
+ ```ruby
122
+ {"id"=>"football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus",
123
+ "type"=>"article",
124
+ "sectionId"=>"football",
125
+ "sectionName"=>"Football",
126
+ "webPublicationDate"=>"2022-06-27T08:42:20Z",
127
+ "webTitle"=>"Football transfer rumours: Chelsea to sign Matthijs de Ligt from Juventus? ",
128
+ "webUrl"=>"https://www.theguardian.com/football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus",
129
+ "apiUrl"=>"https://content.guardianapis.com/football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus",
130
+ "isHosted"=>false,
131
+ "pillarId"=>"pillar/sport",
132
+ "pillarName"=>"Sport"}
133
+ ```
134
+ One element of the `contents` array will be something like:
135
+
136
+ ```ruby
137
+ <GuardianSearcher::Content:0x0000000150b7fe70
138
+ @api_url="https://content.guardianapis.com/football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus",
139
+ @id="football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus",
140
+ @is_hosted=false,
141
+ @pillar_id="pillar/sport",
142
+ @pillar_name="Sport",
143
+ @section_id="football",
144
+ @section_name="Football",
145
+ @type="article",
146
+ @web_publication_date="2022-06-27T08:42:20Z",
147
+ @web_title="Football transfer rumours: Chelsea to sign Matthijs de Ligt from Juventus? ",
148
+ @web_url="https://www.theguardian.com/football/2022/jun/27/football-transfer-rumours-chelsea-to-sign-matthijs-de-ligt-from-juventus">
149
+ ```
150
+
60
151
  ## Development
61
152
 
62
153
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -8,7 +8,10 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = "Alain Mauri"
9
9
  spec.email = "wildeng@hotmail.com"
10
10
 
11
- spec.summary = "A wrapper to search articles from The Guardian"
11
+ spec.summary = "A wrapper to search articles from The Guardian, using its open API.
12
+ You need to register and get your api key to properly use this gem.
13
+ It uses Faraday to make the API calls and has some classes that should help in formatting
14
+ the results as easy to manage Ruby object."
12
15
  spec.homepage = "https://alainmauri.eu"
13
16
  spec.license = "MIT"
14
17
  spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
@@ -30,6 +33,7 @@ Gem::Specification.new do |spec|
30
33
  # spec.add_dependency "example-gem", "~> 1.0"
31
34
  spec.add_dependency "faraday", "~> 2.2"
32
35
 
36
+ spec.add_development_dependency "byebug", "~> 11"
33
37
  spec.add_development_dependency "guard", "~> 2.18"
34
38
  spec.add_development_dependency "guard-bundler", "~> 3.0"
35
39
  spec.add_development_dependency "guard-rspec", "~> 4.7"
@@ -1,38 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GuardianSearcher
4
+ # Class that handles the basic functionality for the Guardian Reader gem
4
5
  class Base
5
6
  include Faraday
6
7
 
7
8
  attr_reader :api_key
8
- attr_accessor :base_uri
9
9
 
10
10
  def initialize(api_key: nil)
11
- @base_uri = "https://content.guardianapis.com"
12
-
13
11
  raise GuardianApyKeyError unless api_key
14
12
 
15
13
  @api_key = api_key
16
14
  end
17
15
 
18
16
  # Options needs to be passed following Guardian API docs
19
- def search(q, options = {})
20
- opt = build_options(options)
17
+ def search(query, options = {})
18
+ url = search_uri + query_string(query, options)
19
+ Faraday.get(url, nil, headers)
20
+ end
21
21
 
22
- url = @base_uri + "/search?q=#{q}&#{opt}&api-key=#{@api_key}"
23
- Faraday.get(url)
22
+ def search_sections(query, options = {})
23
+ url = sections_uri + query_string(query, options)
24
+ Faraday.get(url, nil, headers)
24
25
  end
25
26
 
26
- def search_sections(q, options = {})
27
- opt = build_options(options)
28
- url = @base_uri + "/sections?q=#{q}&#{opt}&api-key=#{@api_key}"
29
- Faraday.get(url)
27
+ def search_tags(query, options = {})
28
+ url = tags_uri + query_string(query, options)
29
+ Faraday.get(url, nil, headers)
30
+ end
31
+
32
+ def search_editions(query, options = {})
33
+ url = editions_uri + query_string(query, options)
34
+ Faraday.get(url, nil, headers)
30
35
  end
31
36
 
32
37
  private
33
38
 
39
+ def base_uri
40
+ "https://content.guardianapis.com"
41
+ end
42
+
43
+ def sections_uri
44
+ "#{base_uri}/sections"
45
+ end
46
+
47
+ def search_uri
48
+ "#{base_uri}/search"
49
+ end
50
+
51
+ def tags_uri
52
+ "#{base_uri}/tags"
53
+ end
54
+
55
+ def editions_uri
56
+ "#{base_uri}/editions"
57
+ end
58
+
59
+ def query_string(q, options = {})
60
+ opt = build_options(options)
61
+ "?q=#{q}&#{opt}"
62
+ end
63
+
34
64
  def build_options(options)
35
65
  Options.new(options).build_options
36
66
  end
67
+
68
+ def headers
69
+ { "api-key" => @api_key }
70
+ end
37
71
  end
38
72
  end
@@ -1,6 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GuardianSearcher
4
- class Content < GuardianSearcher::Base
4
+ class Content
5
+ include GuardianSearcher::Helpers::Util
6
+ def initialize(attributes)
7
+ attributes.each do |key, attribute_value|
8
+ attr_name = key
9
+ attr_name = snakecase(key) unless key.is_a? Symbol
10
+ define_singleton_method("#{attr_name}=".to_sym) do |value|
11
+ instance_variable_set("@#{attr_name}", value)
12
+ end
13
+
14
+ define_singleton_method(attr_name.to_sym) do
15
+ instance_variable_get("@#{attr_name}")
16
+ end
17
+
18
+ send("#{attr_name}=".to_sym, attribute_value)
19
+ end
20
+ end
5
21
  end
6
22
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GuardianSearcher
4
+ module Helpers
5
+ # The class helps generating an array of object from the passed parameters
6
+ # It can be used to generate e.g. an array of Content objects, each one
7
+ # initialised with the data of a single results Hash coming from the Guardian
8
+ # API response
9
+ class Generator
10
+ def generate(results, klass)
11
+ content = []
12
+ results.each do |result|
13
+ content << Object.const_get(klass).new(result)
14
+ end
15
+ content
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GuardianSearcher
4
+ module Helpers
5
+ module Util
6
+ # this method comes from the facets library
7
+ # I took it from there because it was easier for
8
+ # what I have in mind
9
+ #
10
+ # original here https://github.com/rubyworks/facets
11
+ # docs here https://www.rubydoc.info/github/rubyworks/facets/String:snakecase
12
+ def snakecase(key)
13
+ return unless key.is_a? String
14
+
15
+ key.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
16
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
17
+ .tr("-", "_")
18
+ .gsub(/\s/, "_")
19
+ .gsub(/__+/, "_")
20
+ .downcase
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GuardianSearcher
4
- class OptionsNotHashError < StandardError; end
5
- class OptionsNotSupportedError < StandardError; end
6
-
4
+ # the class maps the options passed to the ones needed
5
+ # by the Guardian API for searching
7
6
  class Options < Hash
8
- private attr_accessor :options
9
-
10
7
  def method_missing(method_name, *args, &blk)
11
8
  return options.[](method_name, &blk) if @options.key?(method_name)
12
9
 
13
10
  super(method_name, *args, &blk)
14
11
  end
15
12
 
13
+ def respond_to_missing?(method_name, *args)
14
+ @options.key?(method_name) || super(method_name, *args)
15
+ end
16
+
16
17
  def initialize(options)
18
+ super
17
19
  raise OptionsNotHashError unless options.is_a?(Hash)
18
20
 
19
21
  @options = options
@@ -27,6 +29,7 @@ module GuardianSearcher
27
29
  valid_option?(key)
28
30
  opt += "&#{map_option(key)}=#{value}"
29
31
  end
32
+ return opt
30
33
  end
31
34
 
32
35
  def valid_option?(option)
@@ -41,5 +44,9 @@ module GuardianSearcher
41
44
  page: "page"
42
45
  }[key]
43
46
  end
47
+
48
+ private
49
+
50
+ attr_accessor :options
44
51
  end
45
52
  end
@@ -4,24 +4,45 @@ require "json"
4
4
 
5
5
  module GuardianSearcher
6
6
  class SearchResult
7
- attr_reader :results, :start, :page_size, :pages, :current_page
7
+ attr_reader :results, :start, :page_size, :pages, :current_page, :editions
8
8
 
9
9
  def initialize(
10
10
  current_page: nil,
11
11
  results: nil,
12
12
  page_size: nil,
13
13
  pages: nil,
14
- start: nil
14
+ start: nil,
15
+ editions: nil
15
16
  )
16
-
17
17
  @current_page = current_page
18
18
  @results = results
19
19
  @page_size = page_size
20
20
  @pages = pages
21
21
  @start = start
22
+ @editions = editions
23
+ end
24
+
25
+ def self.parse_with_codes(response: nil)
26
+ raise GuardianSearcher::GuardianSearcherUndefinedResponse unless response
27
+
28
+ case response.status
29
+ when 200
30
+ parse_results(body: response.body)
31
+ when 400
32
+ message = JSON.parse(response.body)["message"]
33
+ raise GuardianSearcher::GuardianBadRequestError, message
34
+ when 401
35
+ message = JSON.parse(response.body)["message"]
36
+ raise GuardianSearcher::GuardianUnauthorizedError, message
37
+ when 500
38
+ message = JSON.parse(response.body)["message"]
39
+ raise GuardianSearcher::GuardianInternalServerError, message
40
+ else
41
+ raise GuardianSearcher::GuardianUnknownError, "Unknown error, check Faraday response"
42
+ end
22
43
  end
23
44
 
24
- def self.parse_results(body: nil)
45
+ def self.parse_results(body:)
25
46
  return unless body
26
47
 
27
48
  body = JSON.parse(body)
@@ -3,18 +3,9 @@
3
3
  require "json"
4
4
 
5
5
  module GuardianSearcher
6
+ # The class parses the search results and creates a new
7
+ # SearchResults object with an editions variable
6
8
  class SectionResult
7
- attr_reader :results, :editions
8
-
9
- def initialize(
10
- results: nil,
11
- editions: nil
12
- )
13
-
14
- @results = results
15
- @editions = editions
16
- end
17
-
18
9
  def self.parse_results(body: nil)
19
10
  return unless body
20
11
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GuardianSearcher
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -3,6 +3,9 @@
3
3
  require_relative "guardian_searcher/version"
4
4
  require "faraday"
5
5
  require_relative "guardian_searcher/base"
6
+ require_relative "guardian_searcher/helpers/util"
7
+ require_relative "guardian_searcher/helpers/generator"
8
+ require_relative "guardian_searcher/content"
6
9
  require_relative "guardian_searcher/search"
7
10
  require_relative "guardian_searcher/search_result"
8
11
  require_relative "guardian_searcher/section_result"
@@ -11,4 +14,11 @@ require_relative "guardian_searcher/options"
11
14
  module GuardianSearcher
12
15
  class Error < StandardError; end
13
16
  class GuardianApyKeyError < StandardError; end
17
+ class GuardianUnauthorizedError < StandardError; end
18
+ class GuardianBadRequestError < StandardError; end
19
+ class GuardianInternalServerError < StandardError; end
20
+ class GuardianUnknownError < StandardError; end
21
+ class GuardianSearcherUndefinedResponse < StandardError; end
22
+ class OptionsNotHashError < StandardError; end
23
+ class OptionsNotSupportedError < StandardError; end
14
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: guardian_searcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Mauri
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-04 00:00:00.000000000 Z
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: guard
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -100,9 +114,11 @@ executables: []
100
114
  extensions: []
101
115
  extra_rdoc_files: []
102
116
  files:
117
+ - ".github/workflows/gem-test.yml"
103
118
  - ".gitignore"
104
119
  - ".rspec"
105
120
  - ".rubocop.yml"
121
+ - ".ruby-version"
106
122
  - CHANGELOG.md
107
123
  - CODE_OF_CONDUCT.md
108
124
  - Gemfile
@@ -117,6 +133,8 @@ files:
117
133
  - lib/guardian_searcher.rb
118
134
  - lib/guardian_searcher/base.rb
119
135
  - lib/guardian_searcher/content.rb
136
+ - lib/guardian_searcher/helpers/generator.rb
137
+ - lib/guardian_searcher/helpers/util.rb
120
138
  - lib/guardian_searcher/options.rb
121
139
  - lib/guardian_searcher/search.rb
122
140
  - lib/guardian_searcher/search_result.rb
@@ -144,8 +162,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
162
  - !ruby/object:Gem::Version
145
163
  version: '0'
146
164
  requirements: []
147
- rubygems_version: 3.2.15
165
+ rubygems_version: 3.5.23
148
166
  signing_key:
149
167
  specification_version: 4
150
- summary: A wrapper to search articles from The Guardian
168
+ summary: A wrapper to search articles from The Guardian, using its open API. You need
169
+ to register and get your api key to properly use this gem. It uses Faraday to make
170
+ the API calls and has some classes that should help in formatting the results as
171
+ easy to manage Ruby object.
151
172
  test_files: []