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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +990 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +81 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/her.gemspec +30 -0
  17. data/lib/her.rb +16 -0
  18. data/lib/her/api.rb +115 -0
  19. data/lib/her/collection.rb +12 -0
  20. data/lib/her/errors.rb +27 -0
  21. data/lib/her/middleware.rb +10 -0
  22. data/lib/her/middleware/accept_json.rb +17 -0
  23. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  24. data/lib/her/middleware/parse_json.rb +21 -0
  25. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  26. data/lib/her/model.rb +72 -0
  27. data/lib/her/model/associations.rb +141 -0
  28. data/lib/her/model/associations/association.rb +103 -0
  29. data/lib/her/model/associations/association_proxy.rb +46 -0
  30. data/lib/her/model/associations/belongs_to_association.rb +96 -0
  31. data/lib/her/model/associations/has_many_association.rb +100 -0
  32. data/lib/her/model/associations/has_one_association.rb +79 -0
  33. data/lib/her/model/attributes.rb +266 -0
  34. data/lib/her/model/base.rb +33 -0
  35. data/lib/her/model/deprecated_methods.rb +61 -0
  36. data/lib/her/model/http.rb +114 -0
  37. data/lib/her/model/introspection.rb +65 -0
  38. data/lib/her/model/nested_attributes.rb +45 -0
  39. data/lib/her/model/orm.rb +205 -0
  40. data/lib/her/model/parse.rb +227 -0
  41. data/lib/her/model/paths.rb +121 -0
  42. data/lib/her/model/relation.rb +164 -0
  43. data/lib/her/version.rb +3 -0
  44. data/spec/api_spec.rb +131 -0
  45. data/spec/collection_spec.rb +26 -0
  46. data/spec/middleware/accept_json_spec.rb +10 -0
  47. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  48. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  49. data/spec/model/associations_spec.rb +416 -0
  50. data/spec/model/attributes_spec.rb +268 -0
  51. data/spec/model/callbacks_spec.rb +145 -0
  52. data/spec/model/dirty_spec.rb +86 -0
  53. data/spec/model/http_spec.rb +194 -0
  54. data/spec/model/introspection_spec.rb +76 -0
  55. data/spec/model/nested_attributes_spec.rb +134 -0
  56. data/spec/model/orm_spec.rb +479 -0
  57. data/spec/model/parse_spec.rb +373 -0
  58. data/spec/model/paths_spec.rb +341 -0
  59. data/spec/model/relation_spec.rb +226 -0
  60. data/spec/model/validations_spec.rb +42 -0
  61. data/spec/model_spec.rb +31 -0
  62. data/spec/spec_helper.rb +26 -0
  63. data/spec/support/extensions/array.rb +5 -0
  64. data/spec/support/extensions/hash.rb +5 -0
  65. data/spec/support/macros/her_macros.rb +17 -0
  66. data/spec/support/macros/model_macros.rb +29 -0
  67. data/spec/support/macros/request_macros.rb +27 -0
  68. metadata +280 -0
@@ -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
@@ -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.
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 3.2.0'
6
+ gem 'activesupport', '~> 3.2.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.0.0'
6
+ gem 'activesupport', '~> 4.0.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.1.0'
6
+ gem 'activesupport', '~> 4.1.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 4.2.0'
6
+ gem 'activesupport', '~> 4.2.0'
7
+ gem 'faraday', '~> 0.8.9'
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,12 @@
1
+ module Her
2
+ class Collection < ::Array
3
+ attr_reader :metadata, :errors
4
+
5
+ # @private
6
+ def initialize(items=[], metadata={}, errors={})
7
+ super(items)
8
+ @metadata = metadata
9
+ @errors = errors
10
+ end
11
+ end
12
+ 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,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