castle-her 1.0.1

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