ddy_remote_resource 0.4.2

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +3 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +182 -0
  10. data/Rakefile +7 -0
  11. data/lib/extensions/ethon/easy/queryable.rb +36 -0
  12. data/lib/remote_resource.rb +64 -0
  13. data/lib/remote_resource/base.rb +126 -0
  14. data/lib/remote_resource/builder.rb +53 -0
  15. data/lib/remote_resource/collection.rb +31 -0
  16. data/lib/remote_resource/connection.rb +24 -0
  17. data/lib/remote_resource/connection_options.rb +41 -0
  18. data/lib/remote_resource/http_errors.rb +33 -0
  19. data/lib/remote_resource/querying/finder_methods.rb +34 -0
  20. data/lib/remote_resource/querying/persistence_methods.rb +38 -0
  21. data/lib/remote_resource/request.rb +106 -0
  22. data/lib/remote_resource/response.rb +69 -0
  23. data/lib/remote_resource/response_handeling.rb +48 -0
  24. data/lib/remote_resource/rest.rb +29 -0
  25. data/lib/remote_resource/url_naming.rb +34 -0
  26. data/lib/remote_resource/url_naming_determination.rb +39 -0
  27. data/lib/remote_resource/version.rb +3 -0
  28. data/remote_resource.gemspec +32 -0
  29. data/spec/lib/extensions/ethon/easy/queryable_spec.rb +135 -0
  30. data/spec/lib/remote_resource/base_spec.rb +388 -0
  31. data/spec/lib/remote_resource/builder_spec.rb +245 -0
  32. data/spec/lib/remote_resource/collection_spec.rb +148 -0
  33. data/spec/lib/remote_resource/connection_options_spec.rb +124 -0
  34. data/spec/lib/remote_resource/connection_spec.rb +61 -0
  35. data/spec/lib/remote_resource/querying/finder_methods_spec.rb +105 -0
  36. data/spec/lib/remote_resource/querying/persistence_methods_spec.rb +174 -0
  37. data/spec/lib/remote_resource/request_spec.rb +594 -0
  38. data/spec/lib/remote_resource/response_spec.rb +196 -0
  39. data/spec/lib/remote_resource/rest_spec.rb +98 -0
  40. data/spec/lib/remote_resource/url_naming_determination_spec.rb +225 -0
  41. data/spec/lib/remote_resource/url_naming_spec.rb +72 -0
  42. data/spec/lib/remote_resource/version_spec.rb +8 -0
  43. data/spec/spec_helper.rb +4 -0
  44. metadata +242 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eb2c03a904db96aa05327826f442deb7b2852228
4
+ data.tar.gz: 4cd7f8229e8a6c7d43d47bc1e4d45c1f3cdb832c
5
+ SHA512:
6
+ metadata.gz: 7390456c69a1ca82d053f44e7966f6f160e0b361af64f4670e2000bb5fe5160feafb2499809be857538c8d1b8e03dc96db218da497c3f73be6e97e5401cec7f3
7
+ data.tar.gz: aa727cdb44ed2933313c2c3681f6fa7e400c66d607be48194c6ade900add787f6754e107885c1fc4a1549a1504641ffa787b12158593b7d634f4ff51418dc0fd
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ remote_resource
@@ -0,0 +1 @@
1
+ 2.1.1
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in remote_resource.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jan van der Pas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,182 @@
1
+ # RemoteResource
2
+
3
+ RemoteResource is a gem to use resources with REST services.
4
+
5
+ ## Goal of RemoteResource
6
+
7
+ To replace `ActiveResource` by providing a dynamic and customizable API interface for REST services.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'remote_resource', git: 'git@lab.digidentity.eu:jvanderpas/remote_resource.git'
15
+ ```
16
+
17
+
18
+ ## Usage
19
+
20
+ Simply include the `RemoteResource::Base` module in the class you want to enable for the REST services.
21
+
22
+ ```ruby
23
+ class ContactPerson
24
+ include RemoteResource::Base
25
+
26
+ self.site = "https://www.myapp.com"
27
+ self.version = '/v2'
28
+ end
29
+ ```
30
+
31
+ ### Options
32
+
33
+ You can set a few options for the `RemoteResource` enabled class.
34
+
35
+
36
+ #### Base URL options (`base_url`)
37
+
38
+ The `base_url` is constructed from the `.site`, `.version`, `.path_prefix`, `.path_postfix`, `.collection`, and `.collection_name` options. The `.collection_name` is automatically constructed from the relative class name.
39
+
40
+ We will use the `ContactPerson` class for these examples, with the `.collection_name` of `'contact_person'`:
41
+
42
+ * `.site`: This sets the URL which should be used to construct the `base_url`.
43
+ * *Example:* `.site = "https://www.myapp.com"`
44
+ * *`base_url`:* `https://www.myapp.com/contact_person`
45
+ * `.version`: This sets the API version for the path, after the `.site` and before the `.path_prefix` that is used to construct the `base_url`.
46
+ * *Example:* `.version = "/api/v2"`
47
+ * *`base_url`:* `https://www.myapp.com/api/v2/contact_person`
48
+ * `.path_prefix`: This sets the prefix for the path, after the `.version` and before the `.collection_name` that is used to construct the `base_url`.
49
+ * *Example:* `.path_prefix = "/registration"`
50
+ * *`base_url`:* `https://www.myapp.com/registration/contact_person`
51
+ * `.path_postfix`: This sets the postfix for the path, after the `.collection_name` that is used to construct the `base_url`.
52
+ * *Example:* `.path_postfix = "/new"`
53
+ * *`base_url`:* `https://www.myapp.com/contact_person/new`
54
+ * `.collection`: This toggles the pluralization of the `collection_name` that is used to construct the `base_url`.
55
+ * *Default:* `false`
56
+ * *Example:* `.collection = true`
57
+ * *`base_url`:* `https://www.myapp.com/contact_persons`
58
+ * `.collection_name`: This sets the `collection_name` that is used to construct the `base_url`.
59
+ * *Example:* `.collection_name = "company"`
60
+ * *`base_url`:* `https://www.myapp.com/company`
61
+
62
+ **override**
63
+
64
+ To override the `base_url` completely, you can use the `base_url` option. This option should be passed into the `connection_options` hash when making a request:
65
+
66
+ * `base_url`: This sets the `base_url`. *note: this does not override the `.content_type` option*
67
+ * *Example:* `{ base_url: "https://api.foo.com/v1" }`
68
+ * *`base_url`:* `https://api.foo.com/v1`
69
+
70
+
71
+ #### Request options
72
+
73
+ Apart from the options which manipulate the `base_url`, there are some more:
74
+
75
+ * `.extra_headers`: This sets the extra headers which are merged with the `.default_headers` and should be used for the request. *note: you can't set the `.default_headers`*
76
+ * *Default:* `.default_headers`: `{ "Content-Type" => "application/json" }`
77
+ * *Example:* `.extra_headers = { "X-Locale" => "en" }`
78
+ * `.headers`: `{ "Content-Type" => "application/json", "X-Locale" => "en" }`
79
+ * `.content_type`: This sets the content-type which should be used for the request URL. *note: this is appended to the `base_url`*
80
+ * *Default:* `".json"`
81
+ * *`base_url`:* `https://www.myapp.com/contact_person`
82
+ * *Example:* `.content-type = ".json"`
83
+ * *Request URL:* `https://www.myapp.com/contact_person.json`
84
+
85
+ #### Body and params options
86
+
87
+ Last but not least, you can pack the request body or params in a `root_element`:
88
+
89
+ * `.root_element`: This sets the `root_element` in which the request body or params should be 'packed' for the request.
90
+ * *Params:* `{ email_address: "foo@bar.com", phone_number: "0031701234567" }`
91
+ * *Example:* `.root_element = :contact_person`
92
+ * *Packed params:* `{ "contact_person" => { email_address: "foo@bar.com", phone_number: "0031701234567" } }`
93
+
94
+
95
+ ### Querying
96
+
97
+ #### Finder methods
98
+
99
+ You can use the `.find`, `.find_by` and `.all` class methods:
100
+
101
+ ```ruby
102
+ # use the `id` as argument
103
+ ContactPerson.find(12)
104
+
105
+ # use a conditions `Hash` as argument
106
+ ContactPerson.find_by(username: 'foobar')
107
+
108
+ # just the whole collection
109
+ ContactPerson.all
110
+ ```
111
+
112
+ To override the given `options`, you can pass in a `connection_options` hash:
113
+
114
+ ```ruby
115
+ connection_options: { root_element: :contact_person, headers: { "X-Locale" => "nl" } }
116
+
117
+ # use the `id` as argument
118
+ ContactPerson.find(12, connection_options)
119
+
120
+ # use a conditions `Hash` as argument
121
+ ContactPerson.find_by((username: 'foobar'), connection_options)
122
+ ```
123
+
124
+ #### Persistence methods
125
+
126
+ You can use the `.create` class method and the `#save` instance method:
127
+
128
+
129
+ ```ruby
130
+ # .create
131
+ ContactPerson.create(username: 'aapmies', first_name: 'Mies')
132
+
133
+ # #save
134
+ contact_person = ContactPerson.new(id: 12)
135
+ contact_person.username = 'aapmies'
136
+ contact_person.save
137
+ ```
138
+ To override the given `options`, you can pass in a `connection_options` hash:
139
+
140
+ ```ruby
141
+ connection_options: { root_element: :contact_person, headers: { "X-Locale" => "nl" } }
142
+
143
+ contact_person = ContactPerson.new(id: 12)
144
+ contact_person.username = 'aapmies'
145
+ contact_person.save(connection_options)
146
+ ```
147
+
148
+ #### REST methods
149
+
150
+ You can use the `.get`, `.put`, `.patch` and `.post` class methods and the `
151
+ #get`, `#put`, `#patch` and `#post` instance methods.
152
+
153
+
154
+ #### With a `connection_options` block
155
+
156
+ You can make your requests in a `connection_options` block. All the requests in the block will use the passed in `connection_options`.
157
+
158
+ ```ruby
159
+ ContactPerson.with_connection_options(headers: { "X-Locale" => "en" }) do
160
+ ContactPerson.find_by(username: 'foobar')
161
+ ContactPerson.find_by(username: 'aapmies', (content-type: '.xml'))
162
+ ContactPerson.find_by((username: 'viking'), (headers: { "X-Locale" => "nl" }))
163
+ end
164
+ ```
165
+
166
+ This will result in two request which use the `{ headers: { "X-Locale" => "en" } }` as `connection_options`, one which will use the `{ headers: { "X-Locale" => "nl" } }` as `connection_options`. And one that will append `.xml` to the request URL.
167
+
168
+ ### Responses
169
+
170
+ The response body of the request will be 'unpacked' from the `root_element` if necessary and parsed. The resulting `Hash` will be used to assign the attributes of the resource.
171
+
172
+ However if you want to access the response of the request, you can use the `#_response` method. This returns a `RemoteResource::Response` object with the `#response_body` and `#response_code` methods.
173
+
174
+
175
+ ```ruby
176
+ contact_person = ContactPerson.find_by((username: 'foobar'), connection_options)
177
+ contact_person._response #=> RemoteResource::Response
178
+ contact_person._response.response_code #=> 200
179
+ contact_person._response.response_body #=> '{"username":"foobar", "name":"Foo", "surname":"Bar"}'
180
+ ```
181
+
182
+
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,36 @@
1
+ # This is a monkey patch to pass Array
2
+ # params without an index.
3
+ #
4
+ # The problem is described in typhoeus/typhoeus issue #320:
5
+ # https://github.com/typhoeus/typhoeus/issues/320
6
+ #
7
+ # The fix is described in dylanfareed/ethon commit 548033a:
8
+ # https://github.com/dylanfareed/ethon/commit/548033a8557a48203b7d49f3f98812bd79bc05e4
9
+ #
10
+
11
+ require 'ethon'
12
+
13
+ module Ethon
14
+ class Easy
15
+ module Queryable
16
+
17
+ private
18
+
19
+ def recursively_generate_pairs(h, prefix, pairs)
20
+ case h
21
+ when Hash
22
+ h.each_pair do |k,v|
23
+ key = prefix.nil? ? k : "#{prefix}[#{k}]"
24
+ pairs_for(v, key, pairs)
25
+ end
26
+ when Array
27
+ h.each_with_index do |v, i|
28
+ key = "#{prefix}[]"
29
+ pairs_for(v, key, pairs)
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,64 @@
1
+ require 'active_support/all'
2
+ require 'active_model'
3
+ require 'virtus'
4
+ require 'typhoeus'
5
+
6
+ require_relative 'extensions/ethon/easy/queryable'
7
+
8
+ require 'remote_resource/version'
9
+ require 'remote_resource/base'
10
+ require 'remote_resource/collection'
11
+ require 'remote_resource/url_naming_determination'
12
+ require 'remote_resource/url_naming'
13
+ require 'remote_resource/connection'
14
+ require 'remote_resource/builder'
15
+ require 'remote_resource/connection_options'
16
+ require 'remote_resource/rest'
17
+ require 'remote_resource/response'
18
+ require 'remote_resource/querying/finder_methods'
19
+ require 'remote_resource/querying/persistence_methods'
20
+ require 'remote_resource/http_errors'
21
+ require 'remote_resource/request'
22
+
23
+
24
+ module RemoteResource
25
+ RemoteResourceError = Class.new StandardError
26
+
27
+ RESTActionUnknown = Class.new RemoteResourceError # REST action
28
+
29
+ class HTTPError < RemoteResourceError # HTTP errors
30
+
31
+ def initialize(response)
32
+ if response.try :response_code
33
+ super "with HTTP response status: #{response.response_code} and response: #{response}"
34
+ else
35
+ super "with HTTP response: #{response}"
36
+ end
37
+ end
38
+ end
39
+
40
+ HTTPRedirectionError = Class.new HTTPError # HTTP 3xx
41
+ HTTPClientError = Class.new HTTPError # HTTP 4xx
42
+ HTTPServerError = Class.new HTTPError # HTTP 5xx
43
+
44
+ HTTPBadRequest = Class.new HTTPClientError # HTTP 400
45
+ HTTPUnauthorized = Class.new HTTPClientError # HTTP 401
46
+ HTTPForbidden = Class.new HTTPClientError # HTTP 403
47
+ HTTPNotFound = Class.new HTTPClientError # HTTP 404
48
+ HTTPMethodNotAllowed = Class.new HTTPClientError # HTTP 405
49
+ HTTPNotAcceptable = Class.new HTTPClientError # HTTP 406
50
+ HTTPRequestTimeout = Class.new HTTPClientError # HTTP 408
51
+ HTTPConflict = Class.new HTTPClientError # HTTP 409
52
+ HTTPGone = Class.new HTTPClientError # HTTP 410
53
+ HTTPTeapot = Class.new HTTPClientError # HTTP 418
54
+
55
+ NginxClientError = Class.new HTTPClientError # HTTP errors used in Nginx
56
+
57
+ HTTPNoResponse = Class.new NginxClientError # HTTP 444
58
+ HTTPRequestHeaderTooLarge = Class.new NginxClientError # HTTP 494
59
+ HTTPCertError = Class.new NginxClientError # HTTP 495
60
+ HTTPNoCert = Class.new NginxClientError # HTTP 496
61
+ HTTPToHTTPS = Class.new NginxClientError # HTTP 497
62
+ HTTPClientClosedRequest = Class.new NginxClientError # HTTP 499
63
+
64
+ end
@@ -0,0 +1,126 @@
1
+ module RemoteResource
2
+ module Base
3
+ extend ActiveSupport::Concern
4
+
5
+ OPTIONS = [:base_url, :site, :headers, :version, :path_prefix, :path_postfix, :content_type, :collection, :collection_name, :root_element]
6
+
7
+ included do
8
+ include Virtus.model
9
+ extend ActiveModel::Naming
10
+ extend ActiveModel::Translation
11
+ include ActiveModel::Conversion
12
+ include ActiveModel::Validations
13
+
14
+ include RemoteResource::Builder
15
+ include RemoteResource::UrlNaming
16
+ include RemoteResource::Connection
17
+ include RemoteResource::REST
18
+
19
+ include RemoteResource::Querying::FinderMethods
20
+ include RemoteResource::Querying::PersistenceMethods
21
+
22
+ attr_accessor :_response
23
+
24
+ attribute :id
25
+ class_attribute :root_element, instance_accessor: false
26
+ end
27
+
28
+ def self.global_headers=(headers)
29
+ Thread.current[:global_headers] = headers
30
+ end
31
+
32
+ def self.global_headers
33
+ Thread.current[:global_headers] ||= {}
34
+ end
35
+
36
+ module ClassMethods
37
+
38
+ def connection_options
39
+ Thread.current[connection_options_thread_name] ||= RemoteResource::ConnectionOptions.new(self)
40
+ end
41
+
42
+ def threaded_connection_options
43
+ Thread.current[threaded_connection_options_thread_name] ||= {}
44
+ end
45
+
46
+ def with_connection_options(connection_options = {})
47
+ begin
48
+ threaded_connection_options
49
+ Thread.current[threaded_connection_options_thread_name].merge! connection_options
50
+ yield
51
+ ensure
52
+ Thread.current[threaded_connection_options_thread_name] = nil
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def threaded_connection_options_thread_name
59
+ "remote_resource.#{_module_name}.threaded_connection_options"
60
+ end
61
+
62
+ def connection_options_thread_name
63
+ "remote_resource.#{_module_name}.connection_options"
64
+ end
65
+
66
+ def _module_name
67
+ self.name.to_s.demodulize.underscore.downcase
68
+ end
69
+ end
70
+
71
+ def connection_options
72
+ @connection_options ||= RemoteResource::ConnectionOptions.new(self.class)
73
+ end
74
+
75
+ def empty?
76
+ _response.try(:sanitized_response_body).blank?
77
+ end
78
+
79
+ def persisted?
80
+ id.present?
81
+ end
82
+
83
+ def new_record?
84
+ !persisted?
85
+ end
86
+
87
+ def success?
88
+ _response.success? && !errors?
89
+ end
90
+
91
+ def errors?
92
+ errors.present?
93
+ end
94
+
95
+ def handle_response(response)
96
+ if response.unprocessable_entity?
97
+ rebuild_resource_from_response(response).tap do |resource|
98
+ resource.assign_errors_from_response response
99
+ end
100
+ else
101
+ rebuild_resource_from_response(response)
102
+ end
103
+ end
104
+
105
+ def assign_response(response)
106
+ @_response = response
107
+ end
108
+
109
+ def assign_errors_from_response(response)
110
+ assign_errors response.error_messages_response_body
111
+ end
112
+
113
+ private
114
+
115
+ def assign_errors(error_messages)
116
+ return unless error_messages.respond_to? :each
117
+
118
+ error_messages.each do |attribute, attribute_errors|
119
+ attribute_errors.each do |error|
120
+ self.errors.add attribute, error
121
+ end
122
+ end
123
+ end
124
+
125
+ end
126
+ end