rest-in-peace 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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