herr 0.7.3
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/.travis.yml +15 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +990 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +81 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/her.gemspec +30 -0
- data/lib/her.rb +16 -0
- data/lib/her/api.rb +115 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +27 -0
- data/lib/her/middleware.rb +10 -0
- data/lib/her/middleware/accept_json.rb +17 -0
- data/lib/her/middleware/first_level_parse_json.rb +36 -0
- data/lib/her/middleware/parse_json.rb +21 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +72 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +103 -0
- data/lib/her/model/associations/association_proxy.rb +46 -0
- data/lib/her/model/associations/belongs_to_association.rb +96 -0
- data/lib/her/model/associations/has_many_association.rb +100 -0
- data/lib/her/model/associations/has_one_association.rb +79 -0
- data/lib/her/model/attributes.rb +266 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +114 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +45 -0
- data/lib/her/model/orm.rb +205 -0
- data/lib/her/model/parse.rb +227 -0
- data/lib/her/model/paths.rb +121 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations_spec.rb +416 -0
- data/spec/model/attributes_spec.rb +268 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +86 -0
- data/spec/model/http_spec.rb +194 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +479 -0
- data/spec/model/parse_spec.rb +373 -0
- data/spec/model/paths_spec.rb +341 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +31 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +29 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +280 -0
data/Rakefile
ADDED
data/UPGRADE.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Upgrade Her
|
2
|
+
|
3
|
+
Here is a list of backward-incompatible changes that were introduced while Her is pre-1.0. After reaching 1.0, it will follow the [Semantic Versioning](http://semver.org/) system.
|
4
|
+
|
5
|
+
## 0.6
|
6
|
+
|
7
|
+
Associations have been refactored so that calling the association name method doesn’t immediately load or fetch the data.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class User
|
11
|
+
include Her::Model
|
12
|
+
has_many :comments
|
13
|
+
end
|
14
|
+
|
15
|
+
# This doesn’t fetch the data yet and it’s still chainable
|
16
|
+
comments = User.find(1).comments
|
17
|
+
|
18
|
+
# This actually fetches the data
|
19
|
+
puts comments.inspect
|
20
|
+
|
21
|
+
# This is no longer possible in her-0.6
|
22
|
+
comments = User.find(1).comments(:approved => 1)
|
23
|
+
|
24
|
+
# To pass additional parameters to the HTTP request, we now have to do this
|
25
|
+
comments = User.find(1).comments.where(:approved => 1)
|
26
|
+
```
|
27
|
+
|
28
|
+
## 0.5
|
29
|
+
|
30
|
+
Her is now compatible with `ActiveModel` and includes `ActiveModel::Validations`.
|
31
|
+
|
32
|
+
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.
|
33
|
+
|
34
|
+
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):
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class User
|
38
|
+
include Her::Model
|
39
|
+
store_response_errors :errors
|
40
|
+
end
|
41
|
+
|
42
|
+
user = User.create(:email => "foo") # POST /users returns { :errors => ["Email is invalid"] }
|
43
|
+
user.errors # => ["Email is invalid"]
|
44
|
+
```
|
45
|
+
|
46
|
+
## 0.2.4
|
47
|
+
|
48
|
+
Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
52
|
+
connection.insert(0, FaradayMiddle::OAuth)
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Now:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
60
|
+
connection.use FaradayMiddle::OAuth
|
61
|
+
connection.use Her::Middleware::FirstLevelParseJSON
|
62
|
+
connection.use Faraday::Request::UrlEncoded
|
63
|
+
connection.use Faraday::Adapter::NetHttp
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
## 0.2
|
68
|
+
|
69
|
+
The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
|
70
|
+
|
71
|
+
```json
|
72
|
+
{ "data": { "id": 1, "name": "Foo" }, "errors": [] }
|
73
|
+
```
|
74
|
+
|
75
|
+
Now it expects this (the `errors` key is not treated as resource data):
|
76
|
+
|
77
|
+
```json
|
78
|
+
{ "id": 1, "name": "Foo", "errors": [] }
|
79
|
+
```
|
80
|
+
|
81
|
+
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/her.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "her/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "herr"
|
7
|
+
s.version = Her::VERSION
|
8
|
+
s.authors = ["Rémi Prévost", "Dermot Haughey"]
|
9
|
+
s.email = ["hderms@gmail.com"]
|
10
|
+
s.homepage = "http://her-rb.org"
|
11
|
+
s.license = "MIT"
|
12
|
+
s.summary = "A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Forked from 'her'"
|
13
|
+
s.description = "Herr is an ORM that maps REST resources and collections to Ruby objects"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency "rake", "~> 10.0"
|
21
|
+
s.add_development_dependency "rspec", "~> 2.13"
|
22
|
+
s.add_development_dependency "rspec-its", "~> 1.0"
|
23
|
+
s.add_development_dependency "fivemat", "~> 1.2"
|
24
|
+
s.add_development_dependency "json", "~> 1.8"
|
25
|
+
|
26
|
+
s.add_runtime_dependency "activemodel", ">= 3.0.0", "<= 4.2"
|
27
|
+
s.add_runtime_dependency "activesupport", ">= 3.0.0", "<= 4.2"
|
28
|
+
s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
|
29
|
+
s.add_runtime_dependency "multi_json", "~> 1.7"
|
30
|
+
end
|
data/lib/her.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "her/version"
|
2
|
+
|
3
|
+
require "multi_json"
|
4
|
+
require "faraday"
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/inflector"
|
7
|
+
require "active_support/core_ext/hash"
|
8
|
+
|
9
|
+
require "her/model"
|
10
|
+
require "her/api"
|
11
|
+
require "her/middleware"
|
12
|
+
require "her/errors"
|
13
|
+
require "her/collection"
|
14
|
+
|
15
|
+
module Her
|
16
|
+
end
|
data/lib/her/api.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
module Her
|
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
|
+
# @private
|
6
|
+
attr_reader :base_uri, :connection, :options
|
7
|
+
|
8
|
+
# Constants
|
9
|
+
FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class].freeze
|
10
|
+
|
11
|
+
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
12
|
+
def self.setup(opts={}, &block)
|
13
|
+
@default_api = new
|
14
|
+
@default_api.setup(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 Her::API.setup to configure the default API
|
19
|
+
#
|
20
|
+
# @example Setting up a new API
|
21
|
+
# api = Her::API.new :url => "https://api.example" do |connection|
|
22
|
+
# connection.use Faraday::Request::UrlEncoded
|
23
|
+
# connection.use Her::Middleware::DefaultParseJSON
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class User
|
27
|
+
# uses_api api
|
28
|
+
# end
|
29
|
+
def initialize(*args, &blk)
|
30
|
+
self.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/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
|
38
|
+
#
|
39
|
+
# @return Faraday::Connection
|
40
|
+
#
|
41
|
+
# @example Setting up the default API connection
|
42
|
+
# Her::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
|
+
# Her::API.setup :url => "https://api.example.com" do |connection|
|
52
|
+
# connection.use Faraday::Request::UrlEncoded
|
53
|
+
# connection.use Her::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
|
+
# Her::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
|
+
@base_uri = opts[:url]
|
75
|
+
@options = opts
|
76
|
+
|
77
|
+
faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
|
78
|
+
@connection = Faraday.new(faraday_options) do |connection|
|
79
|
+
yield connection if block_given?
|
80
|
+
end
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# Define a custom parsing procedure. The procedure is passed the response object and is
|
85
|
+
# expected to return a hash with three keys: a main data Hash, an errors Hash
|
86
|
+
# and a metadata Hash.
|
87
|
+
#
|
88
|
+
# @private
|
89
|
+
def request(opts={})
|
90
|
+
method = opts.delete(:_method)
|
91
|
+
path = opts.delete(:_path)
|
92
|
+
headers = opts.delete(:_headers)
|
93
|
+
opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
|
94
|
+
response = @connection.send method do |request|
|
95
|
+
request.headers.merge!(headers) if headers
|
96
|
+
if method == :get
|
97
|
+
# For GET requests, treat additional parameters as querystring data
|
98
|
+
request.url path, opts
|
99
|
+
else
|
100
|
+
# For POST, PUT and DELETE requests, treat additional parameters as request body
|
101
|
+
request.url path
|
102
|
+
request.body = opts
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
{ :parsed_data => response.env[:body], :response => response }
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
# @private
|
111
|
+
def self.default_api(opts={})
|
112
|
+
defined?(@default_api) ? @default_api : nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/her/errors.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Her
|
2
|
+
module Errors
|
3
|
+
class PathError < StandardError
|
4
|
+
attr_reader :missing_parameter
|
5
|
+
|
6
|
+
def initialize(message, missing_parameter=nil)
|
7
|
+
super(message)
|
8
|
+
@missing_parameter = missing_parameter
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class AssociationUnknownError < StandardError
|
13
|
+
end
|
14
|
+
|
15
|
+
class ParseError < StandardError
|
16
|
+
end
|
17
|
+
|
18
|
+
class ResourceInvalid < StandardError
|
19
|
+
attr_reader :resource
|
20
|
+
def initialize(resource)
|
21
|
+
@resource = resource
|
22
|
+
errors = @resource.response_errors.join(", ")
|
23
|
+
super("Remote validation failed: #{errors}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "her/middleware/parse_json"
|
2
|
+
require "her/middleware/first_level_parse_json"
|
3
|
+
require "her/middleware/second_level_parse_json"
|
4
|
+
require "her/middleware/accept_json"
|
5
|
+
|
6
|
+
module Her
|
7
|
+
module Middleware
|
8
|
+
DefaultParseJSON = FirstLevelParseJSON
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Her
|
2
|
+
module Middleware
|
3
|
+
# This middleware adds a "Accept: application/json" HTTP header
|
4
|
+
class AcceptJSON < Faraday::Middleware
|
5
|
+
# @private
|
6
|
+
def add_header(headers)
|
7
|
+
headers.merge! "Accept" => "application/json"
|
8
|
+
end
|
9
|
+
|
10
|
+
# @private
|
11
|
+
def call(env)
|
12
|
+
add_header(env[:request_headers])
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Her
|
2
|
+
module Middleware
|
3
|
+
# This middleware treat the received first-level JSON structure as the resource data.
|
4
|
+
class FirstLevelParseJSON < ParseJSON
|
5
|
+
# Parse the response body
|
6
|
+
#
|
7
|
+
# @param [String] body The response body
|
8
|
+
# @return [Mixed] the parsed response
|
9
|
+
# @private
|
10
|
+
def parse(body)
|
11
|
+
json = parse_json(body)
|
12
|
+
errors = json.delete(:errors) || {}
|
13
|
+
metadata = json.delete(:metadata) || {}
|
14
|
+
{
|
15
|
+
:data => json,
|
16
|
+
:errors => errors,
|
17
|
+
:metadata => metadata
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# This method is triggered when the response has been received. It modifies
|
22
|
+
# the value of `env[:body]`.
|
23
|
+
#
|
24
|
+
# @param [Hash] env The response environment
|
25
|
+
# @private
|
26
|
+
def on_complete(env)
|
27
|
+
env[:body] = case env[:status]
|
28
|
+
when 204
|
29
|
+
parse('{}')
|
30
|
+
else
|
31
|
+
parse(env[:body])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Her
|
2
|
+
module Middleware
|
3
|
+
class ParseJSON < Faraday::Response::Middleware
|
4
|
+
# @private
|
5
|
+
def parse_json(body = nil)
|
6
|
+
body = '{}' if body.blank?
|
7
|
+
message = "Response from the API must behave like a Hash or an Array (last JSON response was #{body.inspect})"
|
8
|
+
|
9
|
+
json = begin
|
10
|
+
MultiJson.load(body, :symbolize_keys => true)
|
11
|
+
rescue MultiJson::LoadError
|
12
|
+
raise Her::Errors::ParseError, message
|
13
|
+
end
|
14
|
+
|
15
|
+
raise Her::Errors::ParseError, message unless json.is_a?(Hash) or json.is_a?(Array)
|
16
|
+
|
17
|
+
json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Her
|
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
|
+
# 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
|
+
|
14
|
+
{
|
15
|
+
:data => json[:data],
|
16
|
+
:errors => json[:errors],
|
17
|
+
:metadata => json[:metadata]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# This method is triggered when the response has been received. It modifies
|
22
|
+
# the value of `env[:body]`.
|
23
|
+
#
|
24
|
+
# @param [Hash] env The response environment
|
25
|
+
# @private
|
26
|
+
def on_complete(env)
|
27
|
+
env[:body] = case env[:status]
|
28
|
+
when 204
|
29
|
+
parse('{}')
|
30
|
+
else
|
31
|
+
parse(env[:body])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|