herr 0.7.3

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