castle-her 1.0.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 +6 -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 +110 -0
  12. data/castle-her.gemspec +30 -0
  13. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  17. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  18. data/lib/castle-her.rb +20 -0
  19. data/lib/castle-her/api.rb +113 -0
  20. data/lib/castle-her/collection.rb +12 -0
  21. data/lib/castle-her/errors.rb +27 -0
  22. data/lib/castle-her/json_api/model.rb +46 -0
  23. data/lib/castle-her/middleware.rb +12 -0
  24. data/lib/castle-her/middleware/accept_json.rb +17 -0
  25. data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/castle-her/middleware/json_api_parser.rb +36 -0
  27. data/lib/castle-her/middleware/parse_json.rb +21 -0
  28. data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/castle-her/model.rb +75 -0
  30. data/lib/castle-her/model/associations.rb +141 -0
  31. data/lib/castle-her/model/associations/association.rb +103 -0
  32. data/lib/castle-her/model/associations/association_proxy.rb +45 -0
  33. data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
  34. data/lib/castle-her/model/associations/has_many_association.rb +100 -0
  35. data/lib/castle-her/model/associations/has_one_association.rb +79 -0
  36. data/lib/castle-her/model/attributes.rb +284 -0
  37. data/lib/castle-her/model/base.rb +33 -0
  38. data/lib/castle-her/model/deprecated_methods.rb +61 -0
  39. data/lib/castle-her/model/http.rb +114 -0
  40. data/lib/castle-her/model/introspection.rb +65 -0
  41. data/lib/castle-her/model/nested_attributes.rb +45 -0
  42. data/lib/castle-her/model/orm.rb +207 -0
  43. data/lib/castle-her/model/parse.rb +216 -0
  44. data/lib/castle-her/model/paths.rb +126 -0
  45. data/lib/castle-her/model/relation.rb +164 -0
  46. data/lib/castle-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 +166 -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 +290 -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,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.
@@ -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
@@ -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,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,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,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