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