her5 0.8.1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -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 +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +101 -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/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  17. data/her5.gemspec +30 -0
  18. data/lib/her.rb +19 -0
  19. data/lib/her/api.rb +120 -0
  20. data/lib/her/collection.rb +12 -0
  21. data/lib/her/errors.rb +104 -0
  22. data/lib/her/json_api/model.rb +57 -0
  23. data/lib/her/middleware.rb +12 -0
  24. data/lib/her/middleware/accept_json.rb +17 -0
  25. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/her/middleware/json_api_parser.rb +68 -0
  27. data/lib/her/middleware/parse_json.rb +28 -0
  28. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/her/model.rb +75 -0
  30. data/lib/her/model/associations.rb +141 -0
  31. data/lib/her/model/associations/association.rb +107 -0
  32. data/lib/her/model/associations/association_proxy.rb +45 -0
  33. data/lib/her/model/associations/belongs_to_association.rb +101 -0
  34. data/lib/her/model/associations/has_many_association.rb +101 -0
  35. data/lib/her/model/associations/has_one_association.rb +80 -0
  36. data/lib/her/model/attributes.rb +297 -0
  37. data/lib/her/model/base.rb +33 -0
  38. data/lib/her/model/deprecated_methods.rb +61 -0
  39. data/lib/her/model/http.rb +113 -0
  40. data/lib/her/model/introspection.rb +65 -0
  41. data/lib/her/model/nested_attributes.rb +84 -0
  42. data/lib/her/model/orm.rb +207 -0
  43. data/lib/her/model/parse.rb +221 -0
  44. data/lib/her/model/paths.rb +126 -0
  45. data/lib/her/model/relation.rb +164 -0
  46. data/lib/her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +305 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +289 -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,101 @@
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
+ ## 0.8.1 (Note: 0.8.0 yanked)
6
+
7
+ - Initial support for JSONAPI [link](https://github.com/remiprev/her/pull/347)
8
+ - Fix for has_one association parsing [link](https://github.com/remiprev/her/pull/352)
9
+ - Fix for escaping path variables HT @marshall-lee [link](https://github.com/remiprev/her/pull/354)
10
+ - Fix syntax highlighting in README HT @tippenein [link](https://github.com/remiprev/her/pull/356)
11
+ - Fix associations with Active Model Serializers HT @minktom [link](https://github.com/remiprev/her/pull/359)
12
+
13
+ ## 0.7.6
14
+
15
+ - Loosen restrictions on ActiveSupport and ActiveModel to accommodate security fixes [link](https://github.com/remiprev/her/commit/8ff641fcdaf14be7cc9b1a6ee6654f27f7dfa34c)
16
+
17
+ ## 0.7.5
18
+
19
+ - Performance fix for responses with large number of objects [link](https://github.com/remiprev/her/pull/337)
20
+ - Bugfix for dirty attributes [link](https://github.com/remiprev/her/commit/70285debc6837a33a3a750c7c4a7251439464b42)
21
+ - 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
22
+ will begin to align our support with the official ruby maintenance schedule.
23
+ - README updates
24
+
25
+ ## 0.6
26
+
27
+ Associations have been refactored so that calling the association name method doesn’t immediately load or fetch the data.
28
+
29
+ ```ruby
30
+ class User
31
+ include Her::Model
32
+ has_many :comments
33
+ end
34
+
35
+ # This doesn’t fetch the data yet and it’s still chainable
36
+ comments = User.find(1).comments
37
+
38
+ # This actually fetches the data
39
+ puts comments.inspect
40
+
41
+ # This is no longer possible in her-0.6
42
+ comments = User.find(1).comments(:approved => 1)
43
+
44
+ # To pass additional parameters to the HTTP request, we now have to do this
45
+ comments = User.find(1).comments.where(:approved => 1)
46
+ ```
47
+
48
+ ## 0.5
49
+
50
+ Her is now compatible with `ActiveModel` and includes `ActiveModel::Validations`.
51
+
52
+ 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.
53
+
54
+ 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):
55
+
56
+ ```ruby
57
+ class User
58
+ include Her::Model
59
+ store_response_errors :errors
60
+ end
61
+
62
+ user = User.create(:email => "foo") # POST /users returns { :errors => ["Email is invalid"] }
63
+ user.errors # => ["Email is invalid"]
64
+ ```
65
+
66
+ ## 0.2.4
67
+
68
+ Her no longer includes default middleware when making HTTP requests. The user has now to define all the needed middleware. Before:
69
+
70
+ ```ruby
71
+ Her::API.setup :url => "https://api.example.com" do |connection|
72
+ connection.insert(0, FaradayMiddle::OAuth)
73
+ end
74
+ ```
75
+
76
+ Now:
77
+
78
+ ```ruby
79
+ Her::API.setup :url => "https://api.example.com" do |connection|
80
+ connection.use FaradayMiddle::OAuth
81
+ connection.use Her::Middleware::FirstLevelParseJSON
82
+ connection.use Faraday::Request::UrlEncoded
83
+ connection.use Faraday::Adapter::NetHttp
84
+ end
85
+ ```
86
+
87
+ ## 0.2
88
+
89
+ The default parser middleware has been replaced to treat first-level JSON data as the resource or collection data. Before it expected this:
90
+
91
+ ```json
92
+ { "data": { "id": 1, "name": "Foo" }, "errors": [] }
93
+ ```
94
+
95
+ Now it expects this (the `errors` key is not treated as resource data):
96
+
97
+ ```json
98
+ { "id": 1, "name": "Foo", "errors": [] }
99
+ ```
100
+
101
+ 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,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => "../"
4
+
5
+ gem 'activemodel', '~> 5.0.0'
6
+ gem 'activesupport', '~> 5.0.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 = "her5"
7
+ s.version = Her::VERSION
8
+ s.authors = ["Rémi Prévost"]
9
+ s.email = ["remi@exomel.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. Her?"
13
+ s.description = "Her5 is an ORM that maps REST resources and collections to Ruby objects. THIS IS ONLY AN UPDATE TO MAKE HER (UNMAINTAINED) WORK WITH RAILS 5 I LOVE YOU"
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", "<= 5.0.0"
27
+ s.add_runtime_dependency "activesupport", ">= 3.0.0", "<= 5.0.0"
28
+ s.add_runtime_dependency "faraday", ">= 0.8", "< 1.0"
29
+ s.add_runtime_dependency "multi_json", "~> 1.7"
30
+ end
@@ -0,0 +1,19 @@
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
+ module JsonApi
17
+ autoload :Model, 'her/json_api/model'
18
+ end
19
+ end
@@ -0,0 +1,120 @@
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
+ @options[:suppress_response_errors] ||= []
75
+
76
+ faraday_options = @options.reject { |key, value| !FARADAY_OPTIONS.include?(key.to_sym) }
77
+ @connection = Faraday.new(faraday_options) do |connection|
78
+ yield connection if block_given?
79
+ end
80
+ self
81
+ end
82
+
83
+ # Define a custom parsing procedure. The procedure is passed the response object and is
84
+ # expected to return a hash with three keys: a main data Hash, an errors Hash
85
+ # and a metadata Hash.
86
+ #
87
+ # @private
88
+ def request(opts={})
89
+ method = opts.delete(:_method)
90
+ path = opts.delete(:_path)
91
+ headers = opts.delete(:_headers)
92
+ opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
93
+
94
+ begin
95
+ response = @connection.send method do |request|
96
+ request.headers.merge!(headers) if headers
97
+ if method == :get
98
+ # For GET requests, treat additional parameters as querystring data
99
+ request.url path, opts
100
+ else
101
+ # For POST, PUT and DELETE requests, treat additional parameters as request body
102
+ request.url path
103
+ request.body = opts
104
+ end
105
+ end
106
+
107
+ { :parsed_data => response.env[:body], :response => response }
108
+
109
+ rescue Her::Errors::ResponseError => e
110
+ raise e unless @options[:suppress_response_errors].include?(e.status)
111
+ end
112
+ end
113
+
114
+ private
115
+ # @private
116
+ def self.default_api(opts={})
117
+ defined?(@default_api) ? @default_api : nil
118
+ end
119
+ end
120
+ 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,104 @@
1
+ module Her
2
+ module Errors
3
+
4
+ # Base class so that all Her errors can be handled generically.
5
+ class Error < StandardError
6
+ end
7
+
8
+ class PathError < Error
9
+ attr_reader :missing_parameter
10
+
11
+ def initialize(message, missing_parameter=nil)
12
+ super(message)
13
+ @missing_parameter = missing_parameter
14
+ end
15
+ end
16
+
17
+ class AssociationUnknownError < Error
18
+ end
19
+
20
+ class ParseError < Error
21
+ end
22
+
23
+ class ResourceInvalid < Error
24
+ attr_reader :resource
25
+ def initialize(resource)
26
+ @resource = resource
27
+ errors = @resource.response_errors.join(", ")
28
+ super("Remote validation failed: #{errors}")
29
+ end
30
+ end
31
+
32
+ # Base class so that response errors can be handled generically.
33
+ class ResponseError < Error
34
+ def status
35
+ 0
36
+ end
37
+ end
38
+
39
+ # Status code 401: authentication required
40
+ class Unauthorized < ResponseError
41
+ def status
42
+ 401
43
+ end
44
+ end
45
+
46
+ # Status code 403: authenticated but not authorized
47
+ class Forbidden < ResponseError
48
+ def status
49
+ 403
50
+ end
51
+ end
52
+
53
+ # Status code 404: resource not found
54
+ class NotFound < ResponseError
55
+ def status
56
+ 404
57
+ end
58
+ end
59
+
60
+ # Status code 500: we blew up
61
+ class ServerError < ResponseError
62
+ def status
63
+ 500
64
+ end
65
+ end
66
+
67
+ # Status code 502: proxy says API has gone away
68
+ class BadGateway < ResponseError
69
+ def status
70
+ 502
71
+ end
72
+ end
73
+
74
+ # Status code 503: API has definitely gone away
75
+ class Unavailable < ResponseError
76
+ def status
77
+ 503
78
+ end
79
+ end
80
+
81
+ # Status code 504: API has probably gone away
82
+ class TimeOut < ResponseError
83
+ def status
84
+ 504
85
+ end
86
+ end
87
+
88
+ def self.exception_class_for_status(status)
89
+ errors = {
90
+ 401 => "Unauthorized",
91
+ 403 => "Forbidden",
92
+ 404 => "NotFound",
93
+ 500 => "ServerError",
94
+ 502 => "BadGateway",
95
+ 503 => "Unavailable",
96
+ 504 => "TimeOut"
97
+ }
98
+ if errors[status]
99
+ "Her::Errors::#{errors[status]}".constantize
100
+ end
101
+ end
102
+
103
+ end
104
+ end