ddy_remote_resource 0.4.2

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