castle-her 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +1017 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +110 -0
- data/castle-her.gemspec +30 -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/gemfiles/Gemfile.activemodel-5.0.x +7 -0
- data/lib/castle-her.rb +20 -0
- data/lib/castle-her/api.rb +113 -0
- data/lib/castle-her/collection.rb +12 -0
- data/lib/castle-her/errors.rb +27 -0
- data/lib/castle-her/json_api/model.rb +46 -0
- data/lib/castle-her/middleware.rb +12 -0
- data/lib/castle-her/middleware/accept_json.rb +17 -0
- data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
- data/lib/castle-her/middleware/json_api_parser.rb +36 -0
- data/lib/castle-her/middleware/parse_json.rb +21 -0
- data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
- data/lib/castle-her/model.rb +75 -0
- data/lib/castle-her/model/associations.rb +141 -0
- data/lib/castle-her/model/associations/association.rb +103 -0
- data/lib/castle-her/model/associations/association_proxy.rb +45 -0
- data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
- data/lib/castle-her/model/associations/has_many_association.rb +100 -0
- data/lib/castle-her/model/associations/has_one_association.rb +79 -0
- data/lib/castle-her/model/attributes.rb +284 -0
- data/lib/castle-her/model/base.rb +33 -0
- data/lib/castle-her/model/deprecated_methods.rb +61 -0
- data/lib/castle-her/model/http.rb +114 -0
- data/lib/castle-her/model/introspection.rb +65 -0
- data/lib/castle-her/model/nested_attributes.rb +45 -0
- data/lib/castle-her/model/orm.rb +207 -0
- data/lib/castle-her/model/parse.rb +216 -0
- data/lib/castle-her/model/paths.rb +126 -0
- data/lib/castle-her/model/relation.rb +164 -0
- data/lib/castle-her/version.rb +3 -0
- data/spec/api_spec.rb +114 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/json_api/model_spec.rb +166 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/json_api_parser_spec.rb +32 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +31 -0
- data/spec/model/associations_spec.rb +504 -0
- data/spec/model/attributes_spec.rb +389 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +91 -0
- data/spec/model/http_spec.rb +158 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +506 -0
- data/spec/model/parse_spec.rb +345 -0
- data/spec/model/paths_spec.rb +347 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +44 -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 +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +290 -0
data/Rakefile
ADDED
data/UPGRADE.md
ADDED
@@ -0,0 +1,110 @@
|
|
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
|
+
## 1.0.1
|
6
|
+
|
7
|
+
- fixed directory structure
|
8
|
+
|
9
|
+
## 1.0.0 (yanked)
|
10
|
+
|
11
|
+
- `activemodel` and `activesupport` 5 support
|
12
|
+
- release gem as `castle-her`
|
13
|
+
|
14
|
+
## 0.8.1 (Note: 0.8.0 yanked)
|
15
|
+
|
16
|
+
- Initial support for JSONAPI [link](https://github.com/remiprev/her/pull/347)
|
17
|
+
- Fix for has_one association parsing [link](https://github.com/remiprev/her/pull/352)
|
18
|
+
- Fix for escaping path variables HT @marshall-lee [link](https://github.com/remiprev/her/pull/354)
|
19
|
+
- Fix syntax highlighting in README HT @tippenein [link](https://github.com/remiprev/her/pull/356)
|
20
|
+
- Fix associations with Active Model Serializers HT @minktom [link](https://github.com/remiprev/her/pull/359)
|
21
|
+
|
22
|
+
## 0.7.6
|
23
|
+
|
24
|
+
- Loosen restrictions on ActiveSupport and ActiveModel to accommodate security fixes [link](https://github.com/remiprev/her/commit/8ff641fcdaf14be7cc9b1a6ee6654f27f7dfa34c)
|
25
|
+
|
26
|
+
## 0.7.5
|
27
|
+
|
28
|
+
- Performance fix for responses with large number of objects [link](https://github.com/remiprev/her/pull/337)
|
29
|
+
- Bugfix for dirty attributes [link](https://github.com/remiprev/her/commit/70285debc6837a33a3a750c7c4a7251439464b42)
|
30
|
+
- 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
|
31
|
+
will begin to align our support with the official ruby maintenance schedule.
|
32
|
+
- README updates
|
33
|
+
|
34
|
+
## 0.6
|
35
|
+
|
36
|
+
Associations have been refactored so that calling the association name method doesn’t immediately load or fetch the data.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class User
|
40
|
+
include Her::Model
|
41
|
+
has_many :comments
|
42
|
+
end
|
43
|
+
|
44
|
+
# This doesn’t fetch the data yet and it’s still chainable
|
45
|
+
comments = User.find(1).comments
|
46
|
+
|
47
|
+
# This actually fetches the data
|
48
|
+
puts comments.inspect
|
49
|
+
|
50
|
+
# This is no longer possible in her-0.6
|
51
|
+
comments = User.find(1).comments(:approved => 1)
|
52
|
+
|
53
|
+
# To pass additional parameters to the HTTP request, we now have to do this
|
54
|
+
comments = User.find(1).comments.where(:approved => 1)
|
55
|
+
```
|
56
|
+
|
57
|
+
## 0.5
|
58
|
+
|
59
|
+
Her is now compatible with `ActiveModel` and includes `ActiveModel::Validations`.
|
60
|
+
|
61
|
+
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.
|
62
|
+
|
63
|
+
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):
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class User
|
67
|
+
include Her::Model
|
68
|
+
store_response_errors :errors
|
69
|
+
end
|
70
|
+
|
71
|
+
user = User.create(:email => "foo") # POST /users returns { :errors => ["Email is invalid"] }
|
72
|
+
user.errors # => ["Email is invalid"]
|
73
|
+
```
|
74
|
+
|
75
|
+
## 0.2.4
|
76
|
+
|
77
|
+
Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
81
|
+
connection.insert(0, FaradayMiddle::OAuth)
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
Now:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
89
|
+
connection.use FaradayMiddle::OAuth
|
90
|
+
connection.use Her::Middleware::FirstLevelParseJSON
|
91
|
+
connection.use Faraday::Request::UrlEncoded
|
92
|
+
connection.use Faraday::Adapter::NetHttp
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
## 0.2
|
97
|
+
|
98
|
+
The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
|
99
|
+
|
100
|
+
```json
|
101
|
+
{ "data": { "id": 1, "name": "Foo" }, "errors": [] }
|
102
|
+
```
|
103
|
+
|
104
|
+
Now it expects this (the `errors` key is not treated as resource data):
|
105
|
+
|
106
|
+
```json
|
107
|
+
{ "id": 1, "name": "Foo", "errors": [] }
|
108
|
+
```
|
109
|
+
|
110
|
+
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/castle-her.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "castle-her/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "castle-her"
|
7
|
+
s.version = Her::VERSION
|
8
|
+
s.authors = ["Rémi Prévost", "Filip Tepper"]
|
9
|
+
s.email = ["remi@exomel.com", "filip@tepper.pl"]
|
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. Her?"
|
13
|
+
s.description = "Her 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", "~> 11.2.2"
|
21
|
+
s.add_development_dependency "rspec", "~> 3.5.0"
|
22
|
+
s.add_development_dependency "rspec-its", "~> 1.0"
|
23
|
+
s.add_development_dependency "fivemat", "~> 1.2"
|
24
|
+
s.add_development_dependency "json", "~> 2.0.1"
|
25
|
+
|
26
|
+
s.add_runtime_dependency "activemodel", ">= 3.0.0", "<= 5.1.0"
|
27
|
+
s.add_runtime_dependency "activesupport", ">= 3.0.0", "<= 5.1.0"
|
28
|
+
s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
|
29
|
+
s.add_runtime_dependency "multi_json", "~> 1.7"
|
30
|
+
end
|
data/lib/castle-her.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
require "castle-her/version"
|
3
|
+
|
4
|
+
require "multi_json"
|
5
|
+
require "faraday"
|
6
|
+
require "active_support"
|
7
|
+
require "active_support/inflector"
|
8
|
+
require "active_support/core_ext/hash"
|
9
|
+
|
10
|
+
require "castle-her/model"
|
11
|
+
require "castle-her/api"
|
12
|
+
require "castle-her/middleware"
|
13
|
+
require "castle-her/errors"
|
14
|
+
require "castle-her/collection"
|
15
|
+
|
16
|
+
module Her
|
17
|
+
module JsonApi
|
18
|
+
autoload :Model, "castle-her/json_api/model"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,113 @@
|
|
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 :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(opts, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
|
17
|
+
# If your application uses only one API, you should use Her::API.setup to configure the default API
|
18
|
+
#
|
19
|
+
# @example Setting up a new API
|
20
|
+
# api = Her::API.new :url => "https://api.example" do |connection|
|
21
|
+
# connection.use Faraday::Request::UrlEncoded
|
22
|
+
# connection.use Her::Middleware::DefaultParseJSON
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# class User
|
26
|
+
# uses_api api
|
27
|
+
# end
|
28
|
+
def initialize(*args, &blk)
|
29
|
+
setup(*args, &blk)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Setup the API connection.
|
33
|
+
#
|
34
|
+
# @param [Hash] opts the Faraday options
|
35
|
+
# @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
|
36
|
+
# @option opts [String] :ssl A hash containing [SSL options](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates)
|
37
|
+
#
|
38
|
+
# @return Faraday::Connection
|
39
|
+
#
|
40
|
+
# @example Setting up the default API connection
|
41
|
+
# Her::API.setup :url => "https://api.example"
|
42
|
+
#
|
43
|
+
# @example A custom middleware added to the default list
|
44
|
+
# class MyAuthentication < Faraday::Middleware
|
45
|
+
# def call(env)
|
46
|
+
# env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
47
|
+
# @app.call(env)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# Her::API.setup :url => "https://api.example.com" do |connection|
|
51
|
+
# connection.use Faraday::Request::UrlEncoded
|
52
|
+
# connection.use Her::Middleware::DefaultParseJSON
|
53
|
+
# connection.use MyAuthentication
|
54
|
+
# connection.use Faraday::Adapter::NetHttp
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @example A custom parse middleware
|
58
|
+
# class MyCustomParser < Faraday::Response::Middleware
|
59
|
+
# def on_complete(env)
|
60
|
+
# json = JSON.parse(env[:body], :symbolize_names => true)
|
61
|
+
# errors = json.delete(:errors) || {}
|
62
|
+
# metadata = json.delete(:metadata) || []
|
63
|
+
# env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
# Her::API.setup :url => "https://api.example.com" do |connection|
|
67
|
+
# connection.use Faraday::Request::UrlEncoded
|
68
|
+
# connection.use MyCustomParser
|
69
|
+
# connection.use Faraday::Adapter::NetHttp
|
70
|
+
# end
|
71
|
+
def setup(opts={}, &blk)
|
72
|
+
opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
|
73
|
+
@options = opts
|
74
|
+
|
75
|
+
faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
|
76
|
+
@connection = Faraday.new(faraday_options) do |connection|
|
77
|
+
yield connection if block_given?
|
78
|
+
end
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Define a custom parsing procedure. The procedure is passed the response object and is
|
83
|
+
# expected to return a hash with three keys: a main data Hash, an errors Hash
|
84
|
+
# and a metadata Hash.
|
85
|
+
#
|
86
|
+
# @private
|
87
|
+
def request(opts={})
|
88
|
+
method = opts.delete(:_method)
|
89
|
+
path = opts.delete(:_path)
|
90
|
+
headers = opts.delete(:_headers)
|
91
|
+
opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
|
92
|
+
response = @connection.send method do |request|
|
93
|
+
request.headers.merge!(headers) if headers
|
94
|
+
if method == :get
|
95
|
+
# For GET requests, treat additional parameters as querystring data
|
96
|
+
request.url path, opts
|
97
|
+
else
|
98
|
+
# For POST, PUT and DELETE requests, treat additional parameters as request body
|
99
|
+
request.url path
|
100
|
+
request.body = opts
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
{ :parsed_data => response.env[:body], :response => response }
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
# @private
|
109
|
+
def self.default_api(opts={})
|
110
|
+
defined?(@default_api) ? @default_api : nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -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,46 @@
|
|
1
|
+
module Her
|
2
|
+
module JsonApi
|
3
|
+
module Model
|
4
|
+
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
include Her::Model
|
8
|
+
|
9
|
+
[:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
|
10
|
+
define_method method do |*args|
|
11
|
+
raise NoMethodError, "Her::JsonApi::Model does not support the #{method} configuration option"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
method_for :update, :patch
|
16
|
+
|
17
|
+
@type = name.demodulize.tableize
|
18
|
+
|
19
|
+
def self.parse(data)
|
20
|
+
data.fetch(:attributes).merge(data.slice(:id))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.to_params(attributes, changes={})
|
24
|
+
request_data = { type: @type }.tap { |request_body|
|
25
|
+
attrs = attributes.dup.symbolize_keys.tap { |filtered_attributes|
|
26
|
+
if her_api.options[:send_only_modified_attributes]
|
27
|
+
filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
|
28
|
+
hash[attribute] = filtered_attributes[attribute]
|
29
|
+
hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
}
|
33
|
+
request_body[:id] = attrs.delete(:id) if attrs[:id]
|
34
|
+
request_body[:attributes] = attrs
|
35
|
+
}
|
36
|
+
{ data: request_data }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.type(type_name)
|
40
|
+
@type = type_name.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "castle-her/middleware/parse_json"
|
2
|
+
require "castle-her/middleware/first_level_parse_json"
|
3
|
+
require "castle-her/middleware/second_level_parse_json"
|
4
|
+
require "castle-her/middleware/accept_json"
|
5
|
+
|
6
|
+
module Her
|
7
|
+
module Middleware
|
8
|
+
DefaultParseJSON = FirstLevelParseJSON
|
9
|
+
|
10
|
+
autoload :JsonApiParser, "castle-her/middleware/json_api_parser"
|
11
|
+
end
|
12
|
+
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
|