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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.rubocop_todo.yml +232 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +55 -0
  8. data/.yardopts +2 -0
  9. data/CONTRIBUTING.md +26 -0
  10. data/Gemfile +10 -0
  11. data/HER_README.md +1065 -0
  12. data/LICENSE +7 -0
  13. data/README.md +7 -0
  14. data/Rakefile +11 -0
  15. data/UPGRADE.md +101 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +6 -0
  17. data/gemfiles/Gemfile.activemodel-5.0 +6 -0
  18. data/gemfiles/Gemfile.activemodel-5.1 +6 -0
  19. data/gemfiles/Gemfile.activemodel-5.2 +6 -0
  20. data/gemfiles/Gemfile.faraday-1.0 +6 -0
  21. data/lib/restorm/api.rb +121 -0
  22. data/lib/restorm/collection.rb +13 -0
  23. data/lib/restorm/errors.rb +29 -0
  24. data/lib/restorm/json_api/model.rb +42 -0
  25. data/lib/restorm/middleware/accept_json.rb +18 -0
  26. data/lib/restorm/middleware/first_level_parse_json.rb +37 -0
  27. data/lib/restorm/middleware/json_api_parser.rb +37 -0
  28. data/lib/restorm/middleware/parse_json.rb +22 -0
  29. data/lib/restorm/middleware/second_level_parse_json.rb +37 -0
  30. data/lib/restorm/middleware.rb +12 -0
  31. data/lib/restorm/model/associations/association.rb +128 -0
  32. data/lib/restorm/model/associations/association_proxy.rb +44 -0
  33. data/lib/restorm/model/associations/belongs_to_association.rb +95 -0
  34. data/lib/restorm/model/associations/has_many_association.rb +100 -0
  35. data/lib/restorm/model/associations/has_one_association.rb +79 -0
  36. data/lib/restorm/model/associations.rb +141 -0
  37. data/lib/restorm/model/attributes.rb +322 -0
  38. data/lib/restorm/model/base.rb +33 -0
  39. data/lib/restorm/model/deprecated_methods.rb +61 -0
  40. data/lib/restorm/model/http.rb +119 -0
  41. data/lib/restorm/model/introspection.rb +67 -0
  42. data/lib/restorm/model/nested_attributes.rb +45 -0
  43. data/lib/restorm/model/orm.rb +299 -0
  44. data/lib/restorm/model/parse.rb +223 -0
  45. data/lib/restorm/model/paths.rb +125 -0
  46. data/lib/restorm/model/relation.rb +209 -0
  47. data/lib/restorm/model.rb +75 -0
  48. data/lib/restorm/version.rb +3 -0
  49. data/lib/restorm.rb +19 -0
  50. data/restorm.gemspec +29 -0
  51. data/spec/api_spec.rb +120 -0
  52. data/spec/collection_spec.rb +41 -0
  53. data/spec/json_api/model_spec.rb +169 -0
  54. data/spec/middleware/accept_json_spec.rb +11 -0
  55. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  56. data/spec/middleware/json_api_parser_spec.rb +52 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/spec/model/associations/association_proxy_spec.rb +29 -0
  59. data/spec/model/associations_spec.rb +911 -0
  60. data/spec/model/attributes_spec.rb +354 -0
  61. data/spec/model/callbacks_spec.rb +176 -0
  62. data/spec/model/dirty_spec.rb +133 -0
  63. data/spec/model/http_spec.rb +201 -0
  64. data/spec/model/introspection_spec.rb +81 -0
  65. data/spec/model/nested_attributes_spec.rb +135 -0
  66. data/spec/model/orm_spec.rb +704 -0
  67. data/spec/model/parse_spec.rb +520 -0
  68. data/spec/model/paths_spec.rb +348 -0
  69. data/spec/model/relation_spec.rb +247 -0
  70. data/spec/model/validations_spec.rb +43 -0
  71. data/spec/model_spec.rb +45 -0
  72. data/spec/spec_helper.rb +25 -0
  73. data/spec/support/macros/her_macros.rb +17 -0
  74. data/spec/support/macros/model_macros.rb +36 -0
  75. data/spec/support/macros/request_macros.rb +27 -0
  76. 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
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ require "rake"
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new(:spec) do |task|
10
+ task.pattern = "spec/**/*_spec.rb"
11
+ end
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.
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.2.0'
6
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 5.0.0'
6
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 5.1.0'
6
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 5.2.0'
6
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.2.1'
6
+ gem 'faraday', '~> 1.0'
@@ -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,13 @@
1
+ module Restorm
2
+ class Collection < ::Array
3
+
4
+ attr_reader :metadata, :errors
5
+
6
+ # @private
7
+ def initialize(items = [], metadata = {}, errors = {})
8
+ super(items)
9
+ @metadata = metadata
10
+ @errors = errors
11
+ end
12
+ end
13
+ 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