restorm 1.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 +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +232 -0
- data/.ruby-version +1 -0
- data/.travis.yml +55 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/HER_README.md +1065 -0
- data/LICENSE +7 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-4.2 +6 -0
- data/gemfiles/Gemfile.activemodel-5.0 +6 -0
- data/gemfiles/Gemfile.activemodel-5.1 +6 -0
- data/gemfiles/Gemfile.activemodel-5.2 +6 -0
- data/gemfiles/Gemfile.faraday-1.0 +6 -0
- data/lib/restorm/api.rb +121 -0
- data/lib/restorm/collection.rb +13 -0
- data/lib/restorm/errors.rb +29 -0
- data/lib/restorm/json_api/model.rb +42 -0
- data/lib/restorm/middleware/accept_json.rb +18 -0
- data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
- data/lib/restorm/middleware/json_api_parser.rb +37 -0
- data/lib/restorm/middleware/parse_json.rb +22 -0
- data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
- data/lib/restorm/middleware.rb +12 -0
- data/lib/restorm/model/associations/association.rb +128 -0
- data/lib/restorm/model/associations/association_proxy.rb +44 -0
- data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
- data/lib/restorm/model/associations/has_many_association.rb +100 -0
- data/lib/restorm/model/associations/has_one_association.rb +79 -0
- data/lib/restorm/model/associations.rb +141 -0
- data/lib/restorm/model/attributes.rb +322 -0
- data/lib/restorm/model/base.rb +33 -0
- data/lib/restorm/model/deprecated_methods.rb +61 -0
- data/lib/restorm/model/http.rb +119 -0
- data/lib/restorm/model/introspection.rb +67 -0
- data/lib/restorm/model/nested_attributes.rb +45 -0
- data/lib/restorm/model/orm.rb +299 -0
- data/lib/restorm/model/parse.rb +223 -0
- data/lib/restorm/model/paths.rb +125 -0
- data/lib/restorm/model/relation.rb +209 -0
- data/lib/restorm/model.rb +75 -0
- data/lib/restorm/version.rb +3 -0
- data/lib/restorm.rb +19 -0
- data/restorm.gemspec +29 -0
- data/spec/api_spec.rb +120 -0
- data/spec/collection_spec.rb +41 -0
- data/spec/json_api/model_spec.rb +169 -0
- data/spec/middleware/accept_json_spec.rb +11 -0
- data/spec/middleware/first_level_parse_json_spec.rb +63 -0
- data/spec/middleware/json_api_parser_spec.rb +52 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +29 -0
- data/spec/model/associations_spec.rb +911 -0
- data/spec/model/attributes_spec.rb +354 -0
- data/spec/model/callbacks_spec.rb +176 -0
- data/spec/model/dirty_spec.rb +133 -0
- data/spec/model/http_spec.rb +201 -0
- data/spec/model/introspection_spec.rb +81 -0
- data/spec/model/nested_attributes_spec.rb +135 -0
- data/spec/model/orm_spec.rb +704 -0
- data/spec/model/parse_spec.rb +520 -0
- data/spec/model/paths_spec.rb +348 -0
- data/spec/model/relation_spec.rb +247 -0
- data/spec/model/validations_spec.rb +43 -0
- data/spec/model_spec.rb +45 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +203 -0
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012-2015 Rémi Prévost
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Restorm
|
2
|
+
|
3
|
+
Restorm is a fork of [Her](https://github.com/remi/her). Project is renamed so
|
4
|
+
put it in rubygems.org. We have a special need for this to be in rubygems.org as
|
5
|
+
Her was used as a dependency in our gems and we can't add a git repo as a
|
6
|
+
dependency in the gemspec.
|
7
|
+
|
data/Rakefile
ADDED
data/UPGRADE.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Upgrade Her
|
2
|
+
|
3
|
+
Here is a list of notable changes by release. Her follows the [Semantic Versioning](http://semver.org/) system.
|
4
|
+
|
5
|
+
## 0.8.1 (Note: 0.8.0 yanked)
|
6
|
+
|
7
|
+
- Initial support for JSONAPI [link](https://github.com/remiprev/her/pull/347)
|
8
|
+
- Fix for has_one association parsing [link](https://github.com/remiprev/her/pull/352)
|
9
|
+
- Fix for escaping path variables HT @marshall-lee [link](https://github.com/remiprev/her/pull/354)
|
10
|
+
- Fix syntax highlighting in README HT @tippenein [link](https://github.com/remiprev/her/pull/356)
|
11
|
+
- Fix associations with Active Model Serializers HT @minktom [link](https://github.com/remiprev/her/pull/359)
|
12
|
+
|
13
|
+
## 0.7.6
|
14
|
+
|
15
|
+
- Loosen restrictions on ActiveSupport and ActiveModel to accommodate security fixes [link](https://github.com/remiprev/her/commit/8ff641fcdaf14be7cc9b1a6ee6654f27f7dfa34c)
|
16
|
+
|
17
|
+
## 0.7.5
|
18
|
+
|
19
|
+
- Performance fix for responses with large number of objects [link](https://github.com/remiprev/her/pull/337)
|
20
|
+
- Bugfix for dirty attributes [link](https://github.com/remiprev/her/commit/70285debc6837a33a3a750c7c4a7251439464b42)
|
21
|
+
- Add ruby 2.1 and 2.2 to travis test run. We will likely be removing official 1.9.x support in the near future, and
|
22
|
+
will begin to align our support with the official ruby maintenance schedule.
|
23
|
+
- README updates
|
24
|
+
|
25
|
+
## 0.6
|
26
|
+
|
27
|
+
Associations have been refactored so that calling the association name method doesn’t immediately load or fetch the data.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class User
|
31
|
+
include Her::Model
|
32
|
+
has_many :comments
|
33
|
+
end
|
34
|
+
|
35
|
+
# This doesn’t fetch the data yet and it’s still chainable
|
36
|
+
comments = User.find(1).comments
|
37
|
+
|
38
|
+
# This actually fetches the data
|
39
|
+
puts comments.inspect
|
40
|
+
|
41
|
+
# This is no longer possible in her-0.6
|
42
|
+
comments = User.find(1).comments(:approved => 1)
|
43
|
+
|
44
|
+
# To pass additional parameters to the HTTP request, we now have to do this
|
45
|
+
comments = User.find(1).comments.where(:approved => 1)
|
46
|
+
```
|
47
|
+
|
48
|
+
## 0.5
|
49
|
+
|
50
|
+
Her is now compatible with `ActiveModel` and includes `ActiveModel::Validations`.
|
51
|
+
|
52
|
+
Before 0.5, the `errors` method on an object would return an error list received from the server (the `:errors` key defined by the parsing middleware). But now, `errors` returns the error list generated after calling the `valid?` method (or any other similar validation method from `ActiveModel::Validations`). The error list returned from the server is now accessible from the `response_errors` method.
|
53
|
+
|
54
|
+
Since 0.5.5, Her provides a `store_response_errors` method, which allows you to choose the method which will return the response errors. You can use it to revert Her back to its original behavior (ie. `errors` returning the response errors):
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class User
|
58
|
+
include Her::Model
|
59
|
+
store_response_errors :errors
|
60
|
+
end
|
61
|
+
|
62
|
+
user = User.create(:email => "foo") # POST /users returns { :errors => ["Email is invalid"] }
|
63
|
+
user.errors # => ["Email is invalid"]
|
64
|
+
```
|
65
|
+
|
66
|
+
## 0.2.4
|
67
|
+
|
68
|
+
Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
72
|
+
connection.insert(0, FaradayMiddle::OAuth)
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Now:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
80
|
+
connection.use FaradayMiddle::OAuth
|
81
|
+
connection.use Her::Middleware::FirstLevelParseJSON
|
82
|
+
connection.use Faraday::Request::UrlEncoded
|
83
|
+
connection.use Faraday::Adapter::NetHttp
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## 0.2
|
88
|
+
|
89
|
+
The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
|
90
|
+
|
91
|
+
```json
|
92
|
+
{ "data": { "id": 1, "name": "Foo" }, "errors": [] }
|
93
|
+
```
|
94
|
+
|
95
|
+
Now it expects this (the `errors` key is not treated as resource data):
|
96
|
+
|
97
|
+
```json
|
98
|
+
{ "id": 1, "name": "Foo", "errors": [] }
|
99
|
+
```
|
100
|
+
|
101
|
+
If you still want to get the old behavior, you can use `Her::Middleware::SecondLevelParseJSON` instead of `Her::Middleware::FirstLevelParseJSON` in your middleware stack.
|
data/lib/restorm/api.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module Restorm
|
2
|
+
# This class is where all HTTP requests are made. Before using Restorm, you must configure it
|
3
|
+
# so it knows where to make those requests. In Rails, this is usually done in `config/initializers/restorm.rb`:
|
4
|
+
class API
|
5
|
+
|
6
|
+
# @private
|
7
|
+
attr_reader :connection, :options
|
8
|
+
|
9
|
+
# Constants
|
10
|
+
FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class].freeze
|
11
|
+
|
12
|
+
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
13
|
+
def self.setup(opts = {}, &block)
|
14
|
+
@default_api = new(opts, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
|
18
|
+
# If your application uses only one API, you should use Restorm::API.setup to configure the default API
|
19
|
+
#
|
20
|
+
# @example Setting up a new API
|
21
|
+
# api = Restorm::API.new :url => "https://api.example" do |connection|
|
22
|
+
# connection.use Faraday::Request::UrlEncoded
|
23
|
+
# connection.use Restorm::Middleware::DefaultParseJSON
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class User
|
27
|
+
# uses_api api
|
28
|
+
# end
|
29
|
+
def initialize(*args, &blk)
|
30
|
+
setup(*args, &blk)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Setup the API connection.
|
34
|
+
#
|
35
|
+
# @param [Hash] opts the Faraday options
|
36
|
+
# @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
|
37
|
+
# @option opts [String] :ssl A hash containing [SSL options](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
|
38
|
+
#
|
39
|
+
# @return Faraday::Connection
|
40
|
+
#
|
41
|
+
# @example Setting up the default API connection
|
42
|
+
# Restorm::API.setup :url => "https://api.example"
|
43
|
+
#
|
44
|
+
# @example A custom middleware added to the default list
|
45
|
+
# class MyAuthentication < Faraday::Middleware
|
46
|
+
# def call(env)
|
47
|
+
# env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
48
|
+
# @app.call(env)
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
# Restorm::API.setup :url => "https://api.example.com" do |connection|
|
52
|
+
# connection.use Faraday::Request::UrlEncoded
|
53
|
+
# connection.use Restorm::Middleware::DefaultParseJSON
|
54
|
+
# connection.use MyAuthentication
|
55
|
+
# connection.use Faraday::Adapter::NetHttp
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @example A custom parse middleware
|
59
|
+
# class MyCustomParser < Faraday::Response::Middleware
|
60
|
+
# def on_complete(env)
|
61
|
+
# json = JSON.parse(env[:body], :symbolize_names => true)
|
62
|
+
# errors = json.delete(:errors) || {}
|
63
|
+
# metadata = json.delete(:metadata) || []
|
64
|
+
# env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
# Restorm::API.setup :url => "https://api.example.com" do |connection|
|
68
|
+
# connection.use Faraday::Request::UrlEncoded
|
69
|
+
# connection.use MyCustomParser
|
70
|
+
# connection.use Faraday::Adapter::NetHttp
|
71
|
+
# end
|
72
|
+
def setup(opts = {}, &blk)
|
73
|
+
opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
|
74
|
+
@options = opts
|
75
|
+
|
76
|
+
faraday_options = @options.select { |key, _| FARADAY_OPTIONS.include?(key.to_sym) }
|
77
|
+
@connection = Faraday.new(faraday_options) do |connection|
|
78
|
+
yield connection if block_given?
|
79
|
+
end
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Define a custom parsing procedure. The procedure is passed the response object and is
|
84
|
+
# expected to return a hash with three keys: a main data Hash, an errors Hash
|
85
|
+
# and a metadata Hash.
|
86
|
+
#
|
87
|
+
# @private
|
88
|
+
def request(opts = {})
|
89
|
+
method = opts.delete(:_method)
|
90
|
+
path = opts.delete(:_path)
|
91
|
+
headers = opts.delete(:_headers)
|
92
|
+
opts.delete_if { |key, _| key.to_s =~ /^_/ } # Remove all internal parameters
|
93
|
+
if method == :options
|
94
|
+
# Faraday doesn't support the OPTIONS verb because of a name collision with an internal options method
|
95
|
+
# so we need to call run_request directly.
|
96
|
+
request.headers.merge!(headers) if headers
|
97
|
+
response = @connection.run_request method, path, opts, headers
|
98
|
+
else
|
99
|
+
response = @connection.send method do |request|
|
100
|
+
request.headers.merge!(headers) if headers
|
101
|
+
if method == :get
|
102
|
+
# For GET requests, treat additional parameters as querystring data
|
103
|
+
request.url path, opts
|
104
|
+
else
|
105
|
+
# For POST, PUT and DELETE requests, treat additional parameters as request body
|
106
|
+
request.url path
|
107
|
+
request.body = opts
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
{ :parsed_data => response.env[:body], :response => response }
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# @private
|
117
|
+
def self.default_api(opts = {})
|
118
|
+
defined?(@default_api) ? @default_api : nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Errors
|
3
|
+
class PathError < StandardError
|
4
|
+
|
5
|
+
attr_reader :missing_parameter
|
6
|
+
|
7
|
+
def initialize(message, missing_parameter = nil)
|
8
|
+
super(message)
|
9
|
+
@missing_parameter = missing_parameter
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class AssociationUnknownError < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class ParseError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
class ResourceInvalid < StandardError
|
20
|
+
|
21
|
+
attr_reader :resource
|
22
|
+
def initialize(resource)
|
23
|
+
@resource = resource
|
24
|
+
errors = @resource.response_errors.join(", ")
|
25
|
+
super("Remote validation failed: #{errors}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Restorm
|
2
|
+
module JsonApi
|
3
|
+
module Model
|
4
|
+
def self.included(klass)
|
5
|
+
klass.class_eval do
|
6
|
+
include Restorm::Model
|
7
|
+
|
8
|
+
[:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
|
9
|
+
define_method method do |*_|
|
10
|
+
raise NoMethodError, "Restorm::JsonApi::Model does not support the #{method} configuration option"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
method_for :update, :patch
|
15
|
+
|
16
|
+
@type = name.demodulize.tableize
|
17
|
+
|
18
|
+
def self.parse(data)
|
19
|
+
data.fetch(:attributes).merge(data.slice(:id))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.to_params(attributes, changes = {})
|
23
|
+
request_data = { type: @type }.tap do |request_body|
|
24
|
+
attrs = attributes.dup.symbolize_keys.tap do |filtered_attributes|
|
25
|
+
if her_api.options[:send_only_modified_attributes]
|
26
|
+
filtered_attributes.slice! *changes.keys.map(&:to_sym)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
request_body[:id] = attrs.delete(:id) if attrs[:id]
|
30
|
+
request_body[:attributes] = attrs
|
31
|
+
end
|
32
|
+
{ data: request_data }
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.type(type_name)
|
36
|
+
@type = type_name.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Middleware
|
3
|
+
# This middleware adds a "Accept: application/json" HTTP header
|
4
|
+
class AcceptJSON < Faraday::Middleware
|
5
|
+
|
6
|
+
# @private
|
7
|
+
def add_header(headers)
|
8
|
+
headers.merge! "Accept" => "application/json"
|
9
|
+
end
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def call(env)
|
13
|
+
add_header(env[:request_headers])
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Middleware
|
3
|
+
# This middleware treat the received first-level JSON structure as the resource data.
|
4
|
+
class FirstLevelParseJSON < ParseJSON
|
5
|
+
|
6
|
+
# Parse the response body
|
7
|
+
#
|
8
|
+
# @param [String] body The response body
|
9
|
+
# @return [Mixed] the parsed response
|
10
|
+
# @private
|
11
|
+
def parse(body)
|
12
|
+
json = parse_json(body)
|
13
|
+
errors = json.delete(:errors) || {}
|
14
|
+
metadata = json.delete(:metadata) || {}
|
15
|
+
{
|
16
|
+
:data => json,
|
17
|
+
:errors => errors,
|
18
|
+
:metadata => metadata
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# This method is triggered when the response has been received. It modifies
|
23
|
+
# the value of `env[:body]`.
|
24
|
+
#
|
25
|
+
# @param [Hash] env The response environment
|
26
|
+
# @private
|
27
|
+
def on_complete(env)
|
28
|
+
env[:body] = case env[:status]
|
29
|
+
when 204
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Middleware
|
3
|
+
# This middleware expects the resource/collection data to be contained in the `data`
|
4
|
+
# key of the JSON object
|
5
|
+
class JsonApiParser < ParseJSON
|
6
|
+
|
7
|
+
# Parse the response body
|
8
|
+
#
|
9
|
+
# @param [String] body The response body
|
10
|
+
# @return [Mixed] the parsed response
|
11
|
+
# @private
|
12
|
+
def parse(body)
|
13
|
+
json = parse_json(body)
|
14
|
+
|
15
|
+
{
|
16
|
+
:data => json[:data] || {},
|
17
|
+
:errors => json[:errors] || [],
|
18
|
+
:metadata => json[:meta] || {},
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# This method is triggered when the response has been received. It modifies
|
23
|
+
# the value of `env[:body]`.
|
24
|
+
#
|
25
|
+
# @param [Hash] env The response environment
|
26
|
+
# @private
|
27
|
+
def on_complete(env)
|
28
|
+
env[:body] = case env[:status]
|
29
|
+
when 204, 304
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Middleware
|
3
|
+
class ParseJSON < Faraday::Middleware
|
4
|
+
|
5
|
+
# @private
|
6
|
+
def parse_json(body = nil)
|
7
|
+
body = '{}' if body.blank?
|
8
|
+
message = "Response from the API must behave like a Hash or an Array (last JSON response was #{body.inspect})"
|
9
|
+
|
10
|
+
json = begin
|
11
|
+
MultiJson.load(body, :symbolize_keys => true)
|
12
|
+
rescue MultiJson::LoadError
|
13
|
+
raise Restorm::Errors::ParseError, message
|
14
|
+
end
|
15
|
+
|
16
|
+
raise Restorm::Errors::ParseError, message unless json.is_a?(Hash) || json.is_a?(Array)
|
17
|
+
|
18
|
+
json
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Middleware
|
3
|
+
# This middleware expects the resource/collection data to be contained in the `data`
|
4
|
+
# key of the JSON object
|
5
|
+
class SecondLevelParseJSON < ParseJSON
|
6
|
+
|
7
|
+
# Parse the response body
|
8
|
+
#
|
9
|
+
# @param [String] body The response body
|
10
|
+
# @return [Mixed] the parsed response
|
11
|
+
# @private
|
12
|
+
def parse(body)
|
13
|
+
json = parse_json(body)
|
14
|
+
|
15
|
+
{
|
16
|
+
:data => json[:data],
|
17
|
+
:errors => json[:errors],
|
18
|
+
:metadata => json[:metadata]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# This method is triggered when the response has been received. It modifies
|
23
|
+
# the value of `env[:body]`.
|
24
|
+
#
|
25
|
+
# @param [Hash] env The response environment
|
26
|
+
# @private
|
27
|
+
def on_complete(env)
|
28
|
+
env[:body] = case env[:status]
|
29
|
+
when 204
|
30
|
+
parse('{}')
|
31
|
+
else
|
32
|
+
parse(env[:body])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "restorm/middleware/parse_json"
|
2
|
+
require "restorm/middleware/first_level_parse_json"
|
3
|
+
require "restorm/middleware/second_level_parse_json"
|
4
|
+
require "restorm/middleware/accept_json"
|
5
|
+
|
6
|
+
module Restorm
|
7
|
+
module Middleware
|
8
|
+
DefaultParseJSON = FirstLevelParseJSON
|
9
|
+
|
10
|
+
autoload :JsonApiParser, 'restorm/middleware/json_api_parser'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Restorm
|
2
|
+
module Model
|
3
|
+
module Associations
|
4
|
+
class Association
|
5
|
+
|
6
|
+
# @private
|
7
|
+
attr_accessor :params
|
8
|
+
|
9
|
+
# @private
|
10
|
+
def initialize(parent, opts = {})
|
11
|
+
@parent = parent
|
12
|
+
@opts = opts
|
13
|
+
@params = {}
|
14
|
+
|
15
|
+
@klass = @parent.class.restorm_nearby_class(@opts[:class_name])
|
16
|
+
@name = @opts[:name]
|
17
|
+
end
|
18
|
+
|
19
|
+
# @private
|
20
|
+
def self.proxy(parent, opts = {})
|
21
|
+
AssociationProxy.new new(parent, opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @private
|
25
|
+
def self.parse_single(association, klass, data)
|
26
|
+
data_key = association[:data_key]
|
27
|
+
return {} unless data[data_key]
|
28
|
+
|
29
|
+
klass = klass.restorm_nearby_class(association[:class_name])
|
30
|
+
{ association[:name] => klass.instantiate_record(klass, data: data[data_key]) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# @private
|
34
|
+
def assign_single_nested_attributes(attributes)
|
35
|
+
if @parent.attributes[@name].blank?
|
36
|
+
@parent.attributes[@name] = @klass.new(@klass.parse(attributes))
|
37
|
+
else
|
38
|
+
@parent.attributes[@name].assign_attributes(attributes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @private
|
43
|
+
def fetch(opts = {})
|
44
|
+
attribute_value = @parent.attributes[@name]
|
45
|
+
return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && (attribute_value.nil? || !attribute_value.nil? && attribute_value.empty?) && @params.empty?
|
46
|
+
|
47
|
+
return @cached_result unless @params.any? || @cached_result.nil?
|
48
|
+
return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
|
49
|
+
return @opts[:default].try(:dup) if @parent.new?
|
50
|
+
|
51
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}" }
|
52
|
+
@klass.get(path, @params).tap do |result|
|
53
|
+
@cached_result = result unless @params.any?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @private
|
58
|
+
def build_association_path(code)
|
59
|
+
instance_exec(&code)
|
60
|
+
rescue Restorm::Errors::PathError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# @private
|
65
|
+
def reset
|
66
|
+
@params = {}
|
67
|
+
@cached_result = nil
|
68
|
+
@parent.attributes.delete(@name)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Add query parameters to the HTTP request performed to fetch the data
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# class User
|
75
|
+
# include Restorm::Model
|
76
|
+
# has_many :comments
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# user = User.find(1)
|
80
|
+
# user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
|
81
|
+
def where(params = {})
|
82
|
+
return self if params.blank? && @parent.attributes[@name].blank?
|
83
|
+
AssociationProxy.new clone.tap { |a| a.params = a.params.merge(params) }
|
84
|
+
end
|
85
|
+
alias all where
|
86
|
+
|
87
|
+
# Fetches the data specified by id
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# class User
|
91
|
+
# include Restorm::Model
|
92
|
+
# has_many :comments
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# user = User.find(1)
|
96
|
+
# user.comments.find(3) # Fetched via GET "/users/1/comments/3
|
97
|
+
def find(id)
|
98
|
+
return nil if id.blank?
|
99
|
+
path = build_association_path -> { "#{@parent.request_path(@params)}#{@opts[:path]}/#{id}" }
|
100
|
+
@klass.get_resource(path, @params)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Refetches the association and puts the proxy back in its initial state,
|
104
|
+
# which is unloaded. Cached associations are cleared.
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# class User
|
108
|
+
# include Restorm::Model
|
109
|
+
# has_many :comments
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# class Comment
|
113
|
+
# include Restorm::Model
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# user = User.find(1)
|
117
|
+
# user.comments = [#<Comment(comments/2) id=2 body="Hello!">]
|
118
|
+
# user.comments.first.id = "Oops"
|
119
|
+
# user.comments.reload # => [#<Comment(comments/2) id=2 body="Hello!">]
|
120
|
+
# # Fetched again via GET "/users/1/comments"
|
121
|
+
def reload
|
122
|
+
reset
|
123
|
+
fetch
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|