harpy 1.0.0 → 2.0.0.preview1

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
- SHA1:
3
- metadata.gz: 35426c39b7a3ab5a5cbe8a32afd1db109a81f6dc
4
- data.tar.gz: 6869a454bd349bbcd33a1aa7fe7fdce6255f087d
2
+ SHA256:
3
+ metadata.gz: c6bccc44b8e3516964d14dfb259eb7b9294c42449c4f9f72876b5ed1852ed3d0
4
+ data.tar.gz: 989d3494a0b11a5ee04b2c472c2f3ac85920bf42296c3194c1a756f7e1ddd680
5
5
  SHA512:
6
- metadata.gz: 3c730539fe647176feb1c0e1d60a05c8a8df3ac6ecb5e7b44cf8089f976fd1b3896d8f0c94dfd15dc8c47d338ac725b48ddbce6d23eb8b3b856e37a8ad980936
7
- data.tar.gz: f95e41e0b7c2c54f69c324233be772a95ffc79c588d93c38bf8b314f3bef1a0d9a0938831c56a1ed2a23746a9923f3ab8a8aa579a8f2906d0a8a15684dd9f691
6
+ metadata.gz: c0d99509101d673e64f08a18d7dd0ac8335f59e462e60e51638e365fc54b5a11bb5cd43e1454052af17bab69ebb7a94b6b69dbd0de27a5d8932f694a90e61c5b
7
+ data.tar.gz: 83f63ada339c06bd86231d8175b767bf5e56afffd80d7862093189a28e2eb4c86cdf8243b5cd88113fca49715f8f6a94bda439983c024cbef383a171a5ee48d3
@@ -0,0 +1,33 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: docker
10
+ container:
11
+ image: docker.io/library/ruby:${{ matrix.image }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ include:
16
+ - ruby: "3.4"
17
+ image: "3.4"
18
+ - ruby: "4.0"
19
+ image: "4.0"
20
+ - ruby: "4.1"
21
+ image: "4.1-rc"
22
+ steps:
23
+ - name: Install Node.js 24 for the checkout action
24
+ run: |
25
+ apt-get update
26
+ apt-get install -y ca-certificates curl
27
+ curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
28
+ apt-get install -y nodejs
29
+ node --version
30
+ - name: Check out the repository
31
+ uses: actions/checkout@v7
32
+ - run: bundle install
33
+ - run: bundle exec rake
@@ -0,0 +1,20 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ ruby: ["3.4", "4.0", "4.1"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ bundler-cache: true
20
+ - run: bundle exec rake
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  *.gem
2
2
  .bundle
3
3
  pkg/*
4
- *.rbc
4
+ *.rbc
5
+ Gemfile.lock
data/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  Harpy
2
2
  ======
3
3
 
4
- [![Build Status](https://travis-ci.org/TalentBox/harpy.png?branch=master)](https://travis-ci.org/TalentBox/harpy)
5
- [![Code Climate](https://codeclimate.com/github/TalentBox/harpy.png)](https://codeclimate.com/github/TalentBox/harpy)
4
+ [![Test](https://github.com/TalentBox/harpy/actions/workflows/test.yml/badge.svg)](https://github.com/TalentBox/harpy/actions/workflows/test.yml)
6
5
 
7
6
  Client for REST API with HATEOAS
8
7
 
9
8
  Dependencies
10
9
  ------------
11
10
 
12
- * Ruby 1.8.7 or 1.9.2
13
- * gem "typhoeus", "~> 0.2.4"
14
- * gem "activesupport", ">= 3.1.0"
15
- * gem "activemodel", ">= 3.1.0"
16
- * gem "hash-deep-merge", "~> 0.1.1"
11
+ * Ruby >= 3.4.
12
+ * gem "httpx", ">= 1.0"
13
+ * gem "activesupport", ">= 5.2.0"
14
+ * gem "activemodel", ">= 5.2.0"
15
+ * gem "hash-deep-merge", ">= 0.1.1"
17
16
 
18
17
  Usage
19
18
  -----
@@ -44,7 +43,7 @@ Usage
44
43
  # => instance of MyModel with attributes filled in on 200
45
44
  # => nil on 404
46
45
  # => raises Harpy::ClientTimeout on timeout
47
- # => raises Harpy::ClientError on Curl error
46
+ # => raises Harpy::ClientError on HTTP client error
48
47
  # => raises Harpy::InvalidResponseCode on other response codes
49
48
 
50
49
  # Fetch multiple by url in parallel
@@ -60,7 +59,7 @@ Usage
60
59
  }
61
60
  # => return an array of MyModel instances on 200
62
61
  # => raises Harpy::ClientTimeout on timeout
63
- # => raises Harpy::ClientError on Curl error
62
+ # => raises Harpy::ClientError on HTTP client error
64
63
  # => raises Harpy::InvalidResponseCode on other response codes
65
64
 
66
65
  # Search by first_name
data/harpy.gemspec CHANGED
@@ -1,9 +1,10 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
+ require "harpy"
3
4
 
4
5
  Gem::Specification.new do |s|
5
6
  s.name = "harpy"
6
- s.version = "1.0.0"
7
+ s.version = Harpy::VERSION::STRING
7
8
  s.platform = Gem::Platform::RUBY
8
9
  s.authors = ["Joseph HALTER", "Jonathan TRON"]
9
10
  s.email = ["joseph.halter@thetalentbox.com", "jonathan.tron@thetalentbox.com"]
@@ -11,17 +12,19 @@ Gem::Specification.new do |s|
11
12
  s.summary = %q{Client for REST API}
12
13
  s.description = %q{Client for REST API with HATEOAS}
13
14
  s.license = "MIT"
15
+ s.required_ruby_version = ">= 3.4"
14
16
 
15
17
  s.files = `git ls-files`.split("\n")
16
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
20
  s.require_paths = ["lib"]
19
21
 
20
- s.add_runtime_dependency "typhoeus", "~> 0.6.5"
21
- s.add_runtime_dependency "activesupport", [">= 3.1.0"]
22
- s.add_runtime_dependency "activemodel", [">= 3.1.0"]
23
- s.add_runtime_dependency "hash-deep-merge", ["~> 0.1.1"]
22
+ s.add_runtime_dependency "httpx", ">= 1.0"
23
+ s.add_runtime_dependency "activesupport", [">= 5.2.0"]
24
+ s.add_runtime_dependency "activemodel", [">= 5.2.0"]
25
+ s.add_runtime_dependency "hash-deep-merge", [">= 0.1.1"]
24
26
 
25
27
  s.add_development_dependency "rake", [">= 0.8.7"]
26
28
  s.add_development_dependency "rspec"
29
+ s.add_development_dependency "webmock"
27
30
  end
data/lib/harpy/client.rb CHANGED
@@ -1,4 +1,6 @@
1
- require "typhoeus"
1
+ require "base64"
2
+ require "openssl"
3
+ require "httpx"
2
4
  require "hash_deep_merge"
3
5
 
4
6
  module Harpy
@@ -7,6 +9,7 @@ module Harpy
7
9
 
8
10
  def initialize(opts=nil)
9
11
  self.options = (opts || {})
12
+ @session = HTTPX::Session.new
10
13
  end
11
14
 
12
15
  def get(url_or_urls, opts=nil)
@@ -34,33 +37,68 @@ module Harpy
34
37
  end
35
38
 
36
39
  def run(requests)
37
- requests.each{|request| Typhoeus::Hydra.hydra.queue request}
38
- Typhoeus::Hydra.hydra.run
39
- requests.collect(&:response)
40
+ return [] if requests.empty?
41
+
42
+ responses = @session.request(*requests)
43
+ responses.is_a?(Array) ? responses : [responses]
44
+ end
45
+
46
+ def response_status(response)
47
+ response.is_a?(HTTPX::ErrorResponse) ? 0 : response.status
40
48
  end
41
49
 
42
50
  def invalid_code(response)
43
- if response.timed_out?
44
- raise ClientTimeout
45
- elsif response.code.zero?
46
- raise ClientError, response.return_message
51
+ if response.is_a?(HTTPX::ErrorResponse)
52
+ if response.error.is_a?(HTTPX::TimeoutError)
53
+ raise ClientTimeout
54
+ else
55
+ raise ClientError, response.error.message
56
+ end
47
57
  else
48
- raise InvalidResponseCode, response.code.to_s
58
+ raise InvalidResponseCode, response.status.to_s
49
59
  end
50
60
  end
51
61
 
52
62
  private
53
63
 
54
64
  def request(method, urls, opts=nil)
55
- opts = options.deep_merge(opts || {})
65
+ opts = httpx_options(options.deep_merge(opts || {}))
56
66
  case urls
57
67
  when Array
58
- requests = urls.collect do |url|
59
- Typhoeus::Request.new url, opts.merge(:method => method)
68
+ urls.collect do |url|
69
+ @session.build_request(method, url, opts.dup)
60
70
  end
61
71
  else
62
- Typhoeus::Request.send method, urls, opts
72
+ @session.request(method.to_s.upcase, urls, **opts)
73
+ end
74
+ end
75
+
76
+ def httpx_options(opts)
77
+ timeout = opts[:timeout]
78
+ if timeout.is_a?(Numeric)
79
+ opts[:timeout] = {total_request_timeout: timeout.to_f / 1000} if timeout.positive?
80
+ opts.delete(:timeout) unless timeout.positive?
81
+ end
82
+
83
+ ssl_options = (opts[:ssl] || {}).dup
84
+ if opts.key?(:ssl_verifypeer)
85
+ verify_peer = opts.delete(:ssl_verifypeer)
86
+ ssl_options[:verify_mode] = verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
87
+ end
88
+ if opts.key?(:ssl_verifyhost)
89
+ verify_host = opts.delete(:ssl_verifyhost)
90
+ ssl_options[:verify_hostname] = verify_host != false && verify_host != 0
91
+ end
92
+ opts[:ssl] = ssl_options unless ssl_options.empty?
93
+
94
+ username = opts.delete(:username)
95
+ password = opts.delete(:password)
96
+ if username || password
97
+ headers = (opts[:headers] || {}).dup
98
+ headers["Authorization"] ||= "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
99
+ opts[:headers] = headers
63
100
  end
101
+ opts
64
102
  end
65
103
  end
66
104
  end
@@ -10,7 +10,7 @@ module Harpy
10
10
 
11
11
  def resource_url(resource_type)
12
12
  response = Harpy.client.get url
13
- case response.code
13
+ case Harpy.client.response_status(response)
14
14
  when 200
15
15
  body = JSON.parse response.body
16
16
  link = (body["link"] || []).detect{|link| link["rel"] == resource_type}
@@ -22,9 +22,9 @@ module Harpy
22
22
 
23
23
  def urn(urn)
24
24
  response = Harpy.client.get "#{url}/#{urn}"
25
- case response.code
25
+ case Harpy.client.response_status(response)
26
26
  when 301
27
- response.headers_hash["Location"]
27
+ response.headers["location"]
28
28
  when 404
29
29
  nil
30
30
  else
@@ -0,0 +1,14 @@
1
+ module Harpy
2
+ def self.gem_version
3
+ Gem::Version.new VERSION::STRING
4
+ end
5
+
6
+ module VERSION
7
+ MAJOR = 2
8
+ MINOR = 0
9
+ TINY = 0
10
+ PRE = "preview1"
11
+
12
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
13
+ end
14
+ end
@@ -16,6 +16,7 @@ module Harpy
16
16
  base.send :include, ActiveModel::Validations
17
17
  base.send :include, ActiveModel::Validations::Callbacks
18
18
  base.define_model_callbacks :save, :create, :update, :destroy, :only => [:before, :after]
19
+ base.send :include, InstanceMethods
19
20
  end
20
21
 
21
22
  def self.from_url(hash)
@@ -61,7 +62,7 @@ module Harpy
61
62
 
62
63
  def search(conditions={})
63
64
  response = Harpy.client.get url, :params => conditions
64
- case response.code
65
+ case Harpy.client.response_status(response)
65
66
  when 200
66
67
  parsed = JSON.parse response.body
67
68
  items = parsed[resource_name].collect{|model| new model}
@@ -93,6 +94,14 @@ module Harpy
93
94
  delete_from_urn urn id
94
95
  end
95
96
 
97
+ def uman_attribute_name(attr, options = {})
98
+ attr
99
+ end
100
+
101
+ def lookup_ancestors
102
+ [self]
103
+ end
104
+
96
105
  private
97
106
 
98
107
  def url
@@ -100,7 +109,7 @@ module Harpy
100
109
  end
101
110
 
102
111
  def from_url_handler(response)
103
- case response.code
112
+ case Harpy.client.response_status(response)
104
113
  when 200
105
114
  new JSON.parse response.body
106
115
  when 404
@@ -111,7 +120,7 @@ module Harpy
111
120
  end
112
121
 
113
122
  def delete_from_url_handler(response)
114
- case response.code
123
+ case Harpy.client.response_status(response)
115
124
  when 204
116
125
  true
117
126
  when 404
@@ -122,128 +131,136 @@ module Harpy
122
131
  end
123
132
  end
124
133
 
125
- def initialize(attrs = nil)
126
- @attrs = {}
127
- self.attributes = attrs || {}
128
- end
134
+ module InstanceMethods
129
135
 
130
- def attributes=(attrs)
131
- attrs.each do |key, value|
132
- if respond_to? "#{key}="
133
- send "#{key}=", value
134
- else
135
- @attrs[key] = value
136
+ def initialize(attrs = nil)
137
+ @attrs = {}
138
+ self.attributes = attrs || {}
139
+ end
140
+
141
+ def attributes=(attrs)
142
+ attrs.each do |key, value|
143
+ if respond_to? "#{key}="
144
+ send "#{key}=", value
145
+ else
146
+ @attrs[key] = value
147
+ end
136
148
  end
137
149
  end
138
- end
139
150
 
140
- def as_json(*args)
141
- hash = @attrs.dup
142
- hash.delete "link"
143
- hash.delete "urn"
144
- hash
145
- end
151
+ def as_json(*args)
152
+ hash = @attrs.dup
153
+ hash.delete "link"
154
+ hash.delete "urn"
155
+ hash
156
+ end
146
157
 
147
- def save
148
- if valid?
149
- run_callbacks :save do
150
- json = JSON.generate as_json
151
- raise BodyToBig, "Size: #{json.bytesize} bytes (max 1MB)" if json.bytesize > 1.megabyte
152
- persisted? ? update(json) : create(json)
158
+ def save
159
+ if valid?
160
+ run_callbacks :save do
161
+ json = JSON.generate as_json
162
+ raise BodyToBig, "Size: #{json.bytesize} bytes (max 1MB)" if json.bytesize > 1.megabyte
163
+ persisted? ? update(json) : create(json)
164
+ end
165
+ else
166
+ false
153
167
  end
154
- else
155
- false
156
168
  end
157
- end
158
169
 
159
- def destroy
160
- raise Harpy::UrlRequired unless url
161
- run_callbacks :destroy do
162
- process_response Harpy.client.delete(url), :destroy
170
+ def destroy
171
+ raise Harpy::UrlRequired unless url
172
+ run_callbacks :destroy do
173
+ process_response Harpy.client.delete(url), :destroy
174
+ end
163
175
  end
164
- end
165
176
 
166
- def link(rel)
167
- link = (@attrs["link"]||[]).detect{|l| l["rel"]==rel.to_s}
168
- link["href"] if link
169
- end
177
+ def link(rel)
178
+ link = (@attrs["link"]||[]).detect{|l| l["rel"]==rel.to_s}
179
+ link["href"] if link
180
+ end
170
181
 
171
- def url
172
- link "self"
173
- end
182
+ def url
183
+ link "self"
184
+ end
174
185
 
175
- def url_collection
176
- Harpy.entry_point.resource_url self.class.resource_name
177
- end
186
+ def url_collection
187
+ Harpy.entry_point.resource_url self.class.resource_name
188
+ end
178
189
 
179
- def id
180
- @attrs["urn"].split(":").last if @attrs["urn"]
181
- end
190
+ def id
191
+ @attrs["urn"].split(":").last if @attrs["urn"]
192
+ end
182
193
 
183
- def persisted?
184
- @attrs["urn"].present?
185
- end
194
+ def persisted?
195
+ @attrs["urn"].present?
196
+ end
186
197
 
187
- def inspect
188
- "<#{self.class.name} @attrs:#{@attrs.inspect} @errors:#{errors.full_messages.inspect} persisted:#{persisted?}>"
189
- end
198
+ def inspect
199
+ "<#{self.class.name} @attrs:#{@attrs.inspect} @errors:#{errors.full_messages.inspect} persisted:#{persisted?}>"
200
+ end
190
201
 
191
- def has_key?(key)
192
- @attrs.has_key? key.to_s
193
- end
202
+ def has_key?(key)
203
+ @attrs.has_key? key.to_s
204
+ end
194
205
 
195
- def hash
196
- urn.hash
197
- end
206
+ def hash
207
+ urn.hash
208
+ end
198
209
 
199
- def ==(other)
200
- other.equal?(self) || (urn && other.instance_of?(self.class) && other.urn == urn)
201
- end
202
- alias_method :eql?, :==
210
+ def ==(other)
211
+ other.equal?(self) || (urn && other.instance_of?(self.class) && other.urn == urn)
212
+ end
213
+ alias_method :eql?, :==
203
214
 
204
- private
215
+ private
205
216
 
206
- def create(json)
207
- run_callbacks :create do
208
- process_response Harpy.client.post(url_collection, :body => json), :create
217
+ def create(json)
218
+ run_callbacks :create do
219
+ process_response Harpy.client.post(url_collection, :body => json), :create
220
+ end
209
221
  end
210
- end
211
222
 
212
- def update(json)
213
- run_callbacks :update do
214
- raise Harpy::UrlRequired unless url
215
- process_response Harpy.client.put(url, :body => json), :update
223
+ def update(json)
224
+ run_callbacks :update do
225
+ raise Harpy::UrlRequired unless url
226
+ process_response Harpy.client.put(url, :body => json), :update
227
+ end
216
228
  end
217
- end
218
229
 
219
- def process_response(response, context)
220
- case response.code
221
- when 200, 201, 302
222
- @attrs.merge! JSON.parse(response.body)
223
- true
224
- when 204
225
- context==:create ? Harpy.client.invalid_code(response) : true
226
- when 401
227
- raise Harpy::Unauthorized, "Server returned a 401 response code"
228
- when 422
229
- JSON.parse(response.body)["errors"].each do |attr, attr_errors|
230
- attr_errors.each{|attr_error| errors[attr] = attr_error }
230
+ def process_response(response, context)
231
+ case Harpy.client.response_status(response)
232
+ when 200, 201, 302
233
+ @attrs.merge! JSON.parse(response.body)
234
+ true
235
+ when 204
236
+ context==:create ? Harpy.client.invalid_code(response) : true
237
+ when 401
238
+ raise Harpy::Unauthorized, "Server returned a 401 response code"
239
+ when 422
240
+ JSON.parse(response.body)["errors"].each do |attr, attr_errors|
241
+ attr_errors.each{|attr_error| errors.add(attr, :invalid, message: attr_error) }
242
+ end
243
+ false
244
+ else
245
+ Harpy.client.invalid_code response
231
246
  end
232
- false
233
- else
234
- Harpy.client.invalid_code response
235
247
  end
236
- end
237
248
 
238
- def method_missing(method, *args)
239
- key = method.to_s
240
- if persisted? && !@attrs.has_key?(key)
241
- super
242
- elsif key=~/[\]=]\z/
243
- super
244
- else
249
+ def read_attribute_for_validation(key)
245
250
  @attrs[key]
246
251
  end
252
+ public :read_attribute_for_validation
253
+
254
+ def method_missing(method, *args)
255
+ key = method.to_s
256
+ if persisted? && !@attrs.has_key?(key)
257
+ super
258
+ elsif key=~/[\]=]\z/
259
+ super
260
+ else
261
+ @attrs[key]
262
+ end
263
+ end
247
264
  end
248
265
  end
249
266
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
4
+
5
+ module Harpy
6
+ def self.version
7
+ gem_version
8
+ end
9
+ end
data/lib/harpy.rb CHANGED
@@ -14,6 +14,7 @@ module Harpy
14
14
  autoload :Resource, "harpy/resource"
15
15
  autoload :BodyToBig, "harpy/resource"
16
16
  autoload :UnknownResponseCode, "harpy/resource"
17
+ autoload :VERSION, "harpy/gem_version"
17
18
 
18
19
  def self.client=(new_client)
19
20
  @client = new_client
@@ -38,10 +39,10 @@ module Harpy
38
39
  def self.entry_point
39
40
  @entry_point || raise(EntryPointRequired, 'you can setup one with Harpy.entry_point_url = "http://localhost"')
40
41
  end
41
-
42
+
42
43
  def self.reset
43
44
  @client = nil
44
45
  @entry_point = nil
45
46
  end
46
47
 
47
- end
48
+ end