him 0.1.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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. metadata +201 -0
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", "~> 6.1.0"
6
+ gem "activesupport", "~> 6.1.0"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activemodel", "~> 7.0.0"
6
+ gem "activesupport", "~> 7.0.0"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activemodel", "~> 7.1.0"
6
+ gem "activesupport", "~> 7.1.0"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activemodel", "~> 7.2.0"
6
+ gem "activesupport", "~> 7.2.0"
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec path: "../"
4
+
5
+ gem "activemodel", "~> 8.0.0"
6
+ gem "activesupport", "~> 8.0.0"
data/him.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require "him/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "him"
8
+ s.version = Him::VERSION
9
+ s.authors = ["Dale Stevens", "Rémi Prévost"]
10
+ s.email = ["dale@twilightcoders.net"]
11
+ s.homepage = "https://github.com/TwilightCoders/him"
12
+ s.license = "MIT"
13
+ s.summary = "An ORM that maps REST resources to Ruby objects. Forked from Her."
14
+ s.description = "Him is an ORM that maps REST resources and collections to Ruby objects, designed for RESTful API-powered applications."
15
+
16
+ s.required_ruby_version = ">= 3.1"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_development_dependency "rake", ">= 13.0"
24
+ s.add_development_dependency "rspec", "~> 3.5"
25
+
26
+ s.add_runtime_dependency "activemodel", ">= 6.1"
27
+ s.add_runtime_dependency "faraday", ">= 2.0"
28
+ end
data/lib/him/api.rb ADDED
@@ -0,0 +1,121 @@
1
+ module Him
2
+ # This class is where all HTTP requests are made. Before using Her, you must configure it
3
+ # so it knows where to make those requests. In Rails, this is usually done in `config/initializers/her.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 Him::API.setup to configure the default API
19
+ #
20
+ # @example Setting up a new API
21
+ # api = Him::API.new :url => "https://api.example" do |connection|
22
+ # connection.use Faraday::Request::UrlEncoded
23
+ # connection.use Him::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
+ # Him::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
+ # Him::API.setup :url => "https://api.example.com" do |connection|
52
+ # connection.use Faraday::Request::UrlEncoded
53
+ # connection.use Him::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
+ # Him::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,21 @@
1
+ module Him
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
+
13
+ %i[select reject collect map compact flatten uniq reverse
14
+ sort sort_by sample shuffle slice drop take first last].each do |method|
15
+ define_method(method) do |*args, &block|
16
+ result = super(*args, &block)
17
+ result.is_a?(Array) ? self.class.new(result, @metadata, @errors) : result
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/him/errors.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Him
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 Him
2
+ module JsonApi
3
+ module Model
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ include Him::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, "Him::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 Him
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 Him
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,65 @@
1
+ module Him
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
+ included = json.fetch(:included, [])
16
+ primary_data = json.fetch(:data, {})
17
+ Array(primary_data).each do |resource|
18
+ next unless resource.is_a?(Hash)
19
+ relationships = resource.delete(:relationships) { {} }
20
+ resource[:attributes].merge!(resolve_relationships(relationships, included))
21
+ end
22
+
23
+ {
24
+ data: primary_data || {},
25
+ errors: json[:errors] || [],
26
+ metadata: json[:meta] || {}
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def resolve_relationships(relationships, included)
33
+ return {} if included.empty? || relationships.nil?
34
+ relationships.each_with_object({}) do |(rel_name, linkage), built|
35
+ linkage_data = linkage.fetch(:data, {})
36
+ built[rel_name] = if linkage_data.is_a?(Array)
37
+ linkage_data.map { |l| find_included(l, included) }.compact
38
+ else
39
+ find_included(linkage_data, included)
40
+ end
41
+ end
42
+ end
43
+
44
+ def find_included(linkage, included)
45
+ included.detect { |i| i.values_at(:id, :type) == linkage.values_at(:id, :type) }
46
+ end
47
+
48
+ public
49
+
50
+ # This method is triggered when the response has been received. It modifies
51
+ # the value of `env[:body]`.
52
+ #
53
+ # @param [Hash] env The response environment
54
+ # @private
55
+ def on_complete(env)
56
+ env[:body] = case env[:status]
57
+ when 204, 304
58
+ parse('{}')
59
+ else
60
+ parse(env[:body])
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ module Him
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
+ JSON.parse(body, symbolize_names: true)
12
+ rescue JSON::ParserError
13
+ raise Him::Errors::ParseError, message
14
+ end
15
+
16
+ raise Him::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 Him
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 "him/middleware/parse_json"
2
+ require "him/middleware/first_level_parse_json"
3
+ require "him/middleware/second_level_parse_json"
4
+ require "him/middleware/accept_json"
5
+
6
+ module Him
7
+ module Middleware
8
+ DefaultParseJSON = FirstLevelParseJSON
9
+
10
+ autoload :JsonApiParser, "him/middleware/json_api_parser"
11
+ end
12
+ end