rest-in-peace 1.4.0 → 2.0.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,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZmYzY2NhN2JjMjNiZWRlOTZkY2ZiMjY0MzA1MDIzNTI5ZWFjMWYwOQ==
5
+ data.tar.gz: !binary |-
6
+ YjA3MTliNWE4MDA3MDhlYzAzM2M1NzRlMWNhYzVjNzExYmRhNTE1MA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NTU2NWQ2NTc4NGMzMzgxMmY1OTVlYTc1NDE5ODkxMDEyODJkNTliMDhhZTYx
10
+ NzFlZjRmMDdkNTEwODZiMTA1YjJlYmFkMWZlMmIyNmIwNzZmNjdmZTFlNTVi
11
+ ZjE2MWIzZDE1YTI3NWI3ZGM5MDNkOWUxMWEwOWNjYjg3YjM2MjE=
12
+ data.tar.gz: !binary |-
13
+ ZWFlOTFkNTNkMTQ3MTFhMTA2YTBlYjczMzdiZTlmNTE4ZTM5ODhhM2E0ZjY0
14
+ ZDMwYjhhOWMyMTc5YzdjMDUxNWEyOWMzNDE3ZjdmMDU0ODFkMjRlNmNlZjRj
15
+ NzQ0OWUxNmUzYTg4MjQ1M2Y2NTliMjAwNTIwNDcxMTk2ZWRjYTg=
data/README.md CHANGED
@@ -22,6 +22,22 @@ There is no dependency on a specific HTTP client library but the client has been
22
22
 
23
23
  ### Configuration
24
24
 
25
+ #### Attributes
26
+
27
+ You need to specify all the attributes which should be read out of the parsed JSON. You have to specify whether an attribute
28
+ is readonly or writeable:
29
+
30
+ ```ruby
31
+ rest_in_peace do
32
+ attributes do
33
+ read :id
34
+ write :name
35
+ end
36
+ end
37
+ ```
38
+
39
+ #### API Endpoints
40
+
25
41
  You need to define all the API endpoints you want to consume with `RESTinPeace`. Currently the four HTTP Verbs `GET`, `POST`, `PATCH` and `DELETE` are supported.
26
42
 
27
43
  There are two sections where you can specify endpoints: `resource` and `collection`:
@@ -73,6 +89,8 @@ resource.create # calls "POST /rip"
73
89
  resource.reload # calls "GET /rip/1"
74
90
  ```
75
91
 
92
+ **For any writing action (`:post`, `:put`, `:patch`) RESTinPeace will include the writable attributes in the body and `id`.**
93
+
76
94
  #### Collection
77
95
 
78
96
  If you define anything inside the `collection` block, it will define a method on the class:
@@ -107,6 +125,42 @@ end
107
125
 
108
126
  An example pagination mixin with HTTP headers can be found in the [examples directory](https://github.com/ninech/REST-in-Peace/blob/master/examples) of this repo.
109
127
 
128
+ #### ActiveModel Support
129
+
130
+ For easy interoperability with Rails, there is the ability to include ActiveModel into your class. To enable this support, follow these steps:
131
+
132
+ * Define a `create` method (To be called for saving new objects)
133
+ * Define a `save` method (To be called for updates)
134
+ * Call `acts_as_active_model` **after** your *API endpoints* and *attribute* definitions
135
+
136
+ ##### Example
137
+
138
+ ```ruby
139
+ require 'rest_in_peace'
140
+
141
+ module MyClient
142
+ class Fabric < Struct.new(:id, :name, :ip)
143
+ include RESTinPeace
144
+
145
+ rest_in_peace do
146
+ use_api ->() { MyClient.api }
147
+
148
+ attributes do
149
+ read :id
150
+ write :name
151
+ end
152
+
153
+ resource do
154
+ post :create, '/fabrics'
155
+ patch :save, '/fabrics/:id'
156
+ end
157
+
158
+ acts_as_active_model
159
+ end
160
+ end
161
+ end
162
+ ```
163
+
110
164
  #### Complete Configuration
111
165
 
112
166
  ```ruby
@@ -114,12 +168,17 @@ require 'my_client/paginator'
114
168
  require 'rest_in_peace'
115
169
 
116
170
  module MyClient
117
- class Fabric < Struct.new(:id, :name, :ip)
171
+ class Fabric
118
172
  include RESTinPeace
119
173
 
120
174
  rest_in_peace do
121
175
  use_api ->() { MyClient.api }
122
176
 
177
+ attributes do
178
+ read :id
179
+ write :name
180
+ end
181
+
123
182
  resource do
124
183
  patch :save, '/fabrics/:id'
125
184
  post :create, '/fabrics'
@@ -131,6 +190,8 @@ module MyClient
131
190
  get :all, '/fabrics', paginate_with: MyClient::Paginator
132
191
  get :find, '/fabrics/:id'
133
192
  end
193
+
194
+ acts_as_active_model
134
195
  end
135
196
  end
136
197
  end
@@ -149,7 +210,7 @@ ssl_config = {
149
210
  "ca_cert" => "/etc/ssl/certs/ca-chain.crt"
150
211
  }
151
212
 
152
- ssl_config_creator = RESTinPeace::SSLConfigCreator.new(ssl_config, :peer)
213
+ ssl_config_creator = RESTinPeace::Faraday::SSLConfigCreator.new(ssl_config, :peer)
153
214
  ssl_config_creator.faraday_options.inspect
154
215
  # =>
155
216
  {
@@ -158,4 +219,16 @@ ssl_config_creator.faraday_options.inspect
158
219
  :ca_file => "/etc/ssl/certs/ca-chain.crt",
159
220
  :verify_mode => 1
160
221
  }
222
+ ```
223
+
224
+ ### Faraday Middleware: RIP Raise Errors
225
+
226
+ This middleware is mostly equivalent to [this one](https://github.com/lostisland/faraday/blob/cf549f4d883a3cae15db0d835628daa33f6f3a2b/lib/faraday/response/raise_error.rb) but it does not raise an error when the HTTP status code is `422` as this code is used to return validation errors.
227
+
228
+ ```ruby
229
+ Faraday.new do |faraday|
230
+ # ...
231
+ faraday.response :rip_raise_errors
232
+ # ...
233
+ end
161
234
  ```
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0
1
+ 2.0.0
@@ -0,0 +1,72 @@
1
+ require 'active_model'
2
+
3
+ module RESTinPeace
4
+ module ActiveModelAPI
5
+ class MissingMethod < RESTinPeace::DefaultError
6
+ def initialize(method)
7
+ super "No #{method} method has been defined. "\
8
+ 'Maybe you called acts_as_active_model before defining the api endpoints?'
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ check_for_missing_methods(base)
14
+
15
+ base.send(:include, ActiveModel::Dirty)
16
+ base.send(:include, ActiveModel::Conversion)
17
+ base.extend ActiveModel::Naming
18
+
19
+ base.send(:alias_method, :save_without_dirty_tracking, :save)
20
+ base.send(:alias_method, :save, :save_with_dirty_tracking)
21
+
22
+ base.send :define_attribute_methods, base.rip_attributes[:write]
23
+
24
+ base.rip_attributes[:write].each do |attribute|
25
+ base.send(:define_method, "#{attribute}_with_dirty_tracking=") do |value|
26
+ attribute_will_change!(attribute) unless send(attribute) == value
27
+ send("#{attribute}_without_dirty_tracking=", value)
28
+ end
29
+
30
+ base.send(:alias_method, "#{attribute}_without_dirty_tracking=", "#{attribute}=")
31
+ base.send(:alias_method, "#{attribute}=", "#{attribute}_with_dirty_tracking=")
32
+ end
33
+
34
+ def base.human_attribute_name(attr, options = {})
35
+ attr.to_s
36
+ end
37
+
38
+ def base.lookup_ancestors
39
+ [self]
40
+ end
41
+ end
42
+
43
+ def self.check_for_missing_methods(base)
44
+ raise MissingMethod, :save unless base.instance_methods.include?(:save)
45
+ raise MissingMethod, :create unless base.instance_methods.include?(:create)
46
+ end
47
+
48
+ def save_with_dirty_tracking
49
+ save_without_dirty_tracking.tap do
50
+ @changed_attributes.clear if @changed_attributes
51
+ end
52
+ end
53
+
54
+ def persisted?
55
+ !!id
56
+ end
57
+
58
+ def read_attribute_for_validation(attr)
59
+ send(attr)
60
+ end
61
+
62
+ def errors
63
+ @errors ||= ActiveModel::Errors.new(self)
64
+ end
65
+
66
+ def errors=(new_errors)
67
+ new_errors.each do |key, value|
68
+ errors.set(key.to_sym, [value].flatten)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ module RESTinPeace
2
+ class DefinitionProxy
3
+ class AttributesDefinitions
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ def read(*attributes)
9
+ @target.send(:attr_reader, *attributes)
10
+ @target.rip_attributes[:read].concat(attributes)
11
+ end
12
+
13
+ def write(*attributes)
14
+ read(*attributes)
15
+ @target.send(:attr_writer, *attributes)
16
+ @target.rip_attributes[:write].concat(attributes)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -11,6 +11,7 @@ module RESTinPeace
11
11
  def get(method_name, url_template, default_params = {})
12
12
  @target.rip_registry[:collection] << { method: :get, name: method_name, url: url_template }
13
13
  @target.send(:define_singleton_method, method_name) do |given_params = {}|
14
+ raise RESTinPeace::DefinitionProxy::InvalidArgument unless given_params.respond_to?(:merge)
14
15
  params = default_params.merge(given_params)
15
16
 
16
17
  call = RESTinPeace::ApiCall.new(api, url_template, self, params)
@@ -8,7 +8,7 @@ module RESTinPeace
8
8
  def get(method_name, url_template, default_params = {})
9
9
  @target.rip_registry[:resource] << { method: :get, name: method_name, url: url_template }
10
10
  @target.send(:define_method, method_name) do
11
- call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
11
+ call = RESTinPeace::ApiCall.new(api, url_template, self, hash_for_updates)
12
12
  call.get
13
13
  end
14
14
  end
@@ -16,7 +16,7 @@ module RESTinPeace
16
16
  def patch(method_name, url_template)
17
17
  @target.rip_registry[:resource] << { method: :patch, name: method_name, url: url_template }
18
18
  @target.send(:define_method, method_name) do
19
- call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
19
+ call = RESTinPeace::ApiCall.new(api, url_template, self, hash_for_updates)
20
20
  call.patch
21
21
  end
22
22
  end
@@ -24,7 +24,7 @@ module RESTinPeace
24
24
  def post(method_name, url_template)
25
25
  @target.rip_registry[:resource] << { method: :post, name: method_name, url: url_template }
26
26
  @target.send(:define_method, method_name) do
27
- call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
27
+ call = RESTinPeace::ApiCall.new(api, url_template, self, hash_for_updates)
28
28
  call.post
29
29
  end
30
30
  end
@@ -32,16 +32,15 @@ module RESTinPeace
32
32
  def put(method_name, url_template)
33
33
  @target.rip_registry[:resource] << { method: :put, name: method_name, url: url_template }
34
34
  @target.send(:define_method, method_name) do
35
- call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
35
+ call = RESTinPeace::ApiCall.new(api, url_template, self, hash_for_updates)
36
36
  call.put
37
37
  end
38
38
  end
39
39
 
40
- def delete(method_name, url_template, default_params = {})
40
+ def delete(method_name, url_template)
41
41
  @target.rip_registry[:resource] << { method: :delete, name: method_name, url: url_template }
42
- @target.send(:define_method, method_name) do |params = {}|
43
- merged_params = default_params.merge(to_h).merge(params)
44
- call = RESTinPeace::ApiCall.new(api, url_template, self, merged_params)
42
+ @target.send(:define_method, method_name) do
43
+ call = RESTinPeace::ApiCall.new(api, url_template, self, id: id)
45
44
  call.delete
46
45
  end
47
46
  end
@@ -1,8 +1,16 @@
1
1
  require 'rest_in_peace/definition_proxy/resource_method_definitions'
2
2
  require 'rest_in_peace/definition_proxy/collection_method_definitions'
3
+ require 'rest_in_peace/definition_proxy/attributes_definitions'
4
+ require 'rest_in_peace/active_model_api'
3
5
 
4
6
  module RESTinPeace
5
7
  class DefinitionProxy
8
+ class InvalidArgument < RESTinPeace::DefaultError
9
+ def initialize
10
+ super('Given parameter must respond to `merge`.')
11
+ end
12
+ end
13
+
6
14
  def initialize(target)
7
15
  @target = target
8
16
  end
@@ -17,6 +25,19 @@ module RESTinPeace
17
25
  method_definitions.instance_eval(&block)
18
26
  end
19
27
 
28
+ def attributes(&block)
29
+ method_definitions = RESTinPeace::DefinitionProxy::AttributesDefinitions.new(@target)
30
+ method_definitions.instance_eval(&block)
31
+ end
32
+
33
+ def acts_as_active_model
34
+ @target.send(:include, RESTinPeace::ActiveModelAPI)
35
+ end
36
+
37
+ def namespace_attributes_with(namespace)
38
+ @target.rip_namespace = namespace
39
+ end
40
+
20
41
  def use_api(api)
21
42
  @target.api = api
22
43
  end
@@ -0,0 +1,34 @@
1
+ require 'faraday'
2
+
3
+ module RESTinPeace
4
+ module Faraday
5
+ class RaiseErrorsMiddleware < ::Faraday::Response::Middleware
6
+ CLIENT_ERROR_STATUSES = 400...600
7
+
8
+ def on_complete(env)
9
+ case env[:status]
10
+ when 404
11
+ raise Faraday::Error::ResourceNotFound, response_values(env)
12
+ when 407
13
+ # mimic the behavior that we get with proxy requests with HTTPS
14
+ raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
15
+ when 422
16
+ # do not raise an error as 422 from a rails app means validation errors
17
+ # and response body contains the validation errors
18
+ when CLIENT_ERROR_STATUSES
19
+ raise Faraday::Error::ClientError, response_values(env)
20
+ end
21
+ end
22
+
23
+ def response_values(env)
24
+ {
25
+ status: env.status,
26
+ headers: env.response_headers,
27
+ body: env.body,
28
+ }
29
+ end
30
+ end
31
+
32
+ ::Faraday::Response.register_middleware rip_raise_errors: RaiseErrorsMiddleware
33
+ end
34
+ end
@@ -0,0 +1,66 @@
1
+ require 'openssl'
2
+
3
+ module RESTinPeace
4
+ module Faraday
5
+ class SSLConfigCreator
6
+ class MissingParam < Exception; end
7
+
8
+ def initialize(config, verify = :peer)
9
+ @config = config
10
+ @verify = verify
11
+
12
+ raise MissingParam, 'Specify :ca_cert in ssl options' unless @config[:ca_cert]
13
+ raise MissingParam, 'Specify :client_key in ssl options' unless @config[:client_key]
14
+ raise MissingParam, 'Specify :client_cert in ssl options' unless @config[:client_cert]
15
+ end
16
+
17
+ def faraday_options
18
+ {
19
+ client_cert: client_cert,
20
+ client_key: client_key,
21
+ ca_file: ca_cert_path,
22
+ verify_mode: verify_mode,
23
+ }
24
+ end
25
+
26
+ def client_cert
27
+ OpenSSL::X509::Certificate.new(open_file(client_cert_path))
28
+ end
29
+
30
+ def client_cert_path
31
+ path(@config[:client_cert])
32
+ end
33
+
34
+ def client_key
35
+ OpenSSL::PKey::RSA.new(open_file(client_key_path))
36
+ end
37
+
38
+ def client_key_path
39
+ path(@config[:client_key])
40
+ end
41
+
42
+ def ca_cert_path
43
+ path(@config[:ca_cert])
44
+ end
45
+
46
+ def verify_mode
47
+ case @verify
48
+ when :peer
49
+ OpenSSL::SSL::VERIFY_PEER
50
+ else
51
+ raise "Unknown verify variant '#{@verify}'"
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def open_file(file)
58
+ File.open(file)
59
+ end
60
+
61
+ def path(file)
62
+ File.join(file)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,35 +1,51 @@
1
1
  module RESTinPeace
2
2
  class ResponseConverter
3
- def initialize(response, klass)
4
- @response = response
5
- @klass = klass
3
+ class UnknownConvertStrategy < RESTinPeace::DefaultError
4
+ def initialize(klass)
5
+ super("Don't know how to convert #{klass}")
6
+ end
7
+ end
8
+
9
+ attr_accessor :body, :klass, :existing_instance
10
+
11
+ def initialize(response, instance_or_class)
12
+ self.body = response.body
13
+
14
+ if instance_or_class.respond_to?(:new)
15
+ self.klass = instance_or_class
16
+ self.existing_instance = new_instance
17
+ else
18
+ self.klass = instance_or_class.class
19
+ self.existing_instance = instance_or_class
20
+ end
6
21
  end
7
22
 
8
23
  def result
9
- case @response.body.class.to_s
24
+ case body.class.to_s
10
25
  when 'Array'
11
26
  convert_from_array
12
27
  when 'Hash'
13
28
  convert_from_hash
14
29
  when 'String'
15
- @response.body
30
+ body
16
31
  else
17
- raise "Don't know how to convert #{@response.body.class}"
32
+ raise UnknownConvertStrategy, body.class
18
33
  end
19
34
  end
20
35
 
21
36
  def convert_from_array
22
- @response.body.map do |entity|
23
- convert_from_hash(entity)
37
+ body.map do |entity|
38
+ convert_from_hash(entity, new_instance)
24
39
  end
25
40
  end
26
41
 
27
- def convert_from_hash(entity = @response.body)
28
- klass.new entity
42
+ def convert_from_hash(entity = body, instance = existing_instance)
43
+ instance.force_attributes_from_hash entity
44
+ instance
29
45
  end
30
46
 
31
- def klass
32
- @klass.respond_to?(:new) ? @klass : @klass.class
47
+ def new_instance
48
+ klass.new
33
49
  end
34
50
  end
35
51
  end
@@ -16,7 +16,7 @@ module RESTinPeace
16
16
  tokens.each do |token|
17
17
  param = @params.delete(token.to_sym)
18
18
  raise IncompleteParams, "Unknown parameter for token :#{token} found" unless param
19
- @url.gsub!(%r{:#{token}}, param.to_s)
19
+ @url.sub!(%r{:#{token}}, param.to_s)
20
20
  end
21
21
  @url
22
22
  end
data/lib/rest_in_peace.rb CHANGED
@@ -11,28 +11,57 @@ module RESTinPeace
11
11
  end
12
12
 
13
13
  def initialize(attributes = {})
14
- update_from_hash(attributes)
14
+ force_attributes_from_hash(attributes)
15
15
  end
16
16
 
17
- def to_h
18
- Hash[each_pair.to_a]
17
+ def hash_for_updates
18
+ hash_representation = { id: id }
19
+ self.class.rip_attributes[:write].map do |key|
20
+ value = send(key)
21
+ hash_representation[key] = hash_representation_of_object(value)
22
+ end
23
+ if self.class.rip_namespace
24
+ { id: id, self.class.rip_namespace => hash_representation }
25
+ else
26
+ hash_representation
27
+ end
19
28
  end
20
29
 
21
30
  def update_attributes(attributes)
22
- update_from_hash(attributes)
31
+ attributes.each do |key, value|
32
+ next unless respond_to?("#{key}=")
33
+ send("#{key}=", value)
34
+ end
23
35
  end
24
36
 
25
- protected
37
+ def to_h
38
+ hash_representation = {}
39
+ self.class.rip_attributes.values.flatten.each do |attr|
40
+ hash_representation[attr] = send(attr)
41
+ end
42
+ hash_representation
43
+ end
26
44
 
27
- def update_from_hash(hash)
28
- hash.each do |key, value|
29
- next unless self.class.members.map(&:to_s).include?(key.to_s)
30
- send("#{key}=", value)
45
+ def force_attributes_from_hash(attributes)
46
+ attributes.each do |key, value|
47
+ next unless respond_to?(key)
48
+ if respond_to?("#{key}=")
49
+ send("#{key}=", value)
50
+ else
51
+ instance_variable_set("@#{key}", value)
52
+ end
31
53
  end
32
54
  end
33
55
 
56
+ def hash_representation_of_object(object)
57
+ return object.hash_for_updates if object.respond_to?(:hash_for_updates)
58
+ return object.map { |element| hash_representation_of_object(element) } if object.is_a?(Array)
59
+ object
60
+ end
61
+
34
62
  module ClassMethods
35
63
  attr_accessor :api
64
+ attr_accessor :rip_namespace
36
65
 
37
66
  def rest_in_peace(&block)
38
67
  definition_proxy = RESTinPeace::DefinitionProxy.new(self)
@@ -45,5 +74,12 @@ module RESTinPeace
45
74
  collection: [],
46
75
  }
47
76
  end
77
+
78
+ def rip_attributes
79
+ @rip_attributes ||= {
80
+ read: [],
81
+ write: [],
82
+ }
83
+ end
48
84
  end
49
85
  end
@@ -17,9 +17,11 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ['lib']
19
19
 
20
+ s.add_runtime_dependency 'activemodel', '> 3.2', '<= 4.1'
21
+
20
22
  s.add_development_dependency 'rake', '~> 10.0'
21
23
  s.add_development_dependency 'rspec', '~> 3.0'
22
- s.add_development_dependency 'guard', '~> 2.6.1'
23
- s.add_development_dependency 'guard-rspec', '~> 4.2.0'
24
- s.add_development_dependency 'simplecov', '~> 0.8.2'
24
+ s.add_development_dependency 'guard', '~> 2.6', '>= 2.6.1'
25
+ s.add_development_dependency 'guard-rspec', '~> 4.2', '>= 4.2.0'
26
+ s.add_development_dependency 'simplecov', '~> 0.8', '>= 0.8.2'
25
27
  end