her 0.6.3 → 0.6.4

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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ODI2OTY3MmY1ZmU0OTRiYWJmZmQ5NTk5NTczYzgxYmJhOGVlY2M3Mg==
4
+ NzFmYjMyNGNlODUyZjk4N2I2MzA5ZGZmYjdkNTQyYTQ5YzZiMzlhMw==
5
5
  data.tar.gz: !binary |-
6
- Mjg2ZTEwOGExNGZjZTcwYTVjOGQ2YjhiOWIwMDUzZjMzZTllNDc4Zg==
6
+ ODJkYzM0YTU3YTI3YTI2ZmNlNmIxM2UxOTAzZTc2YTA3NTJjMTM0Zg==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MDI1OGYxM2U2ODA3MTRiZGE4NmQ3ZTczMzRiYzAyOTA0NGI5MDYxNWZhMzg0
10
- OWI2ODJkMDlhMWY5NThkZmZkNjhhMGVmYjRmY2JkOWU4ZWM2ZmZhYjczMTVk
11
- ODg1ODMxOWJhZDZlNjVkZmE1YWE3NjQ4MDFkMjY0MmMxNzNiOTQ=
9
+ ODEzZjJhOTYwNDM0OGI0NGQzYzBmMGVkODdiYjk3NDFkY2E5ZTNkZTRkYWZl
10
+ MDlhODA5ZDY4NTJhZjE2MDM1MDIzMjViZDIzMjUxMTdhZDA1OTE4NDliODRj
11
+ MDdkMzdiMTI2MzAzNDE4NjkxNDY5ZDRhNmFmNGJjMjlmODcyNjA=
12
12
  data.tar.gz: !binary |-
13
- N2U0Yzc3NWM2OGI0MzYwOTdkNjRiNWY2N2M2NGVmNWY5NTMzMmQwZGFiMGVm
14
- Zjg3YWUzNDU4ZmM2ZmU1NmE2N2QzNTE4N2FkY2E2Mjk3NTU5ODhlMWE4MTk3
15
- ZDM4OGM3NTgyNDA2ZmM5YzQ5MmY0NGM4OGMzNWFlZGFjZjczY2U=
13
+ MTBiODVmMmEzMDI5NTI2NmE0NGMzNDIyNWJkNjRiZTM2YTAxYzc1YjFhMTA4
14
+ YTM5NWRmNzVhNjE2Yjc0MDVkYTNiMjYyY2NkMTg2ODYxYjJhZDdlZjYzNmVk
15
+ NmYyNDc2M2IwYmVlNWU1YjU3Y2ZmYTE0YmQ1YWMyYjE4ZGM1YzQ=
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --protected
2
+ --no-private
data/README.md CHANGED
@@ -19,6 +19,8 @@ That’s it!
19
19
 
20
20
  ## Usage
21
21
 
22
+ _For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her)._
23
+
22
24
  First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with these lines:
23
25
 
24
26
  ```ruby
@@ -98,7 +100,7 @@ user = User.new(fullname: "Maeby Fünke")
98
100
  user.save
99
101
  ```
100
102
 
101
- You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her. For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her).
103
+ You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
102
104
 
103
105
  ## Middleware
104
106
 
data/her.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
7
7
  s.version = Her::VERSION
8
8
  s.authors = ["Rémi Prévost"]
9
9
  s.email = ["remi@exomel.com"]
10
- s.homepage = "http://remiprev.github.com/her"
10
+ s.homepage = "http://her-rb.org"
11
11
  s.license = "MIT"
12
12
  s.summary = "A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Her?"
13
13
  s.description = "Her is an ORM that maps REST resources and collections to Ruby objects"
data/lib/her/api.rb CHANGED
@@ -6,9 +6,9 @@ module Her
6
6
  attr_reader :base_uri, :connection, :options
7
7
 
8
8
  # Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
9
- def self.setup(attrs={}, &block)
9
+ def self.setup(opts={}, &block)
10
10
  @default_api = new
11
- @default_api.setup(attrs, &block)
11
+ @default_api.setup(opts, &block)
12
12
  end
13
13
 
14
14
  # Create a new API object. This is useful to create multiple APIs and use them with the `uses_api` method.
@@ -29,9 +29,9 @@ module Her
29
29
 
30
30
  # Setup the API connection.
31
31
  #
32
- # @param [Hash] attrs the Faraday options
33
- # @option attrs [String] :url The main HTTP API root (eg. `https://api.example.com`)
34
- # @option attrs [String] :ssl A hash containing [SSL options](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
32
+ # @param [Hash] opts the Faraday options
33
+ # @option opts [String] :url The main HTTP API root (eg. `https://api.example.com`)
34
+ # @option opts [String] :ssl A hash containing [SSL options](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates)
35
35
  #
36
36
  # @return Faraday::Connection
37
37
  #
@@ -65,10 +65,10 @@ module Her
65
65
  # connection.use MyCustomParser
66
66
  # connection.use Faraday::Adapter::NetHttp
67
67
  # end
68
- def setup(attrs={}, &blk)
69
- attrs[:url] = attrs.delete(:base_uri) if attrs.include?(:base_uri) # Support legacy :base_uri option
70
- @base_uri = attrs[:url]
71
- @options = attrs
68
+ def setup(opts={}, &blk)
69
+ opts[:url] = opts.delete(:base_uri) if opts.include?(:base_uri) # Support legacy :base_uri option
70
+ @base_uri = opts[:url]
71
+ @options = opts
72
72
  @connection = Faraday.new(@options) do |connection|
73
73
  yield connection if block_given?
74
74
  end
@@ -80,20 +80,20 @@ module Her
80
80
  # and a metadata Hash.
81
81
  #
82
82
  # @private
83
- def request(attrs={})
84
- method = attrs.delete(:_method)
85
- path = attrs.delete(:_path)
86
- headers = attrs.delete(:_headers)
87
- attrs.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
83
+ def request(opts={})
84
+ method = opts.delete(:_method)
85
+ path = opts.delete(:_path)
86
+ headers = opts.delete(:_headers)
87
+ opts.delete_if { |key, value| key.to_s =~ /^_/ } # Remove all internal parameters
88
88
  response = @connection.send method do |request|
89
89
  request.headers.merge!(headers) if headers
90
90
  if method == :get
91
91
  # For GET requests, treat additional parameters as querystring data
92
- request.url path, attrs
92
+ request.url path, opts
93
93
  else
94
94
  # For POST, PUT and DELETE requests, treat additional parameters as request body
95
95
  request.url path
96
- request.body = attrs
96
+ request.body = opts
97
97
  end
98
98
  end
99
99
 
@@ -102,7 +102,7 @@ module Her
102
102
 
103
103
  private
104
104
  # @private
105
- def self.default_api(attrs={})
105
+ def self.default_api(opts={})
106
106
  defined?(@default_api) ? @default_api : nil
107
107
  end
108
108
  end
@@ -2,10 +2,12 @@ module Her
2
2
  module Middleware
3
3
  # This middleware adds a "Accept: application/json" HTTP header
4
4
  class AcceptJSON < Faraday::Middleware
5
+ # @private
5
6
  def add_header(headers)
6
7
  headers.merge! "Accept" => "application/json"
7
8
  end
8
9
 
10
+ # @private
9
11
  def call(env)
10
12
  add_header(env[:request_headers])
11
13
  @app.call(env)
@@ -6,6 +6,7 @@ module Her
6
6
  #
7
7
  # @param [String] body The response body
8
8
  # @return [Mixed] the parsed response
9
+ # @private
9
10
  def parse(body)
10
11
  json = parse_json(body)
11
12
  errors = json.delete(:errors) || {}
@@ -21,6 +22,7 @@ module Her
21
22
  # the value of `env[:body]`.
22
23
  #
23
24
  # @param [Hash] env The response environment
25
+ # @private
24
26
  def on_complete(env)
25
27
  env[:body] = case env[:status]
26
28
  when 204
@@ -1,6 +1,7 @@
1
1
  module Her
2
2
  module Middleware
3
3
  class ParseJSON < Faraday::Response::Middleware
4
+ # @private
4
5
  def parse_json(body = nil)
5
6
  body ||= '{}'
6
7
  message = "Response from the API must behave like a Hash or an Array (last JSON response was #{body.inspect})"
@@ -7,6 +7,7 @@ module Her
7
7
  #
8
8
  # @param [String] body The response body
9
9
  # @return [Mixed] the parsed response
10
+ # @private
10
11
  def parse(body)
11
12
  json = parse_json(body)
12
13
 
@@ -21,6 +22,7 @@ module Her
21
22
  # the value of `env[:body]`.
22
23
  #
23
24
  # @param [Hash] env The response environment
25
+ # @private
24
26
  def on_complete(env)
25
27
  env[:body] = case env[:status]
26
28
  when 204
data/lib/her/model.rb CHANGED
@@ -38,6 +38,7 @@ module Her
38
38
  include Her::Model::NestedAttributes
39
39
 
40
40
  # Supported ActiveModel modules
41
+ include ActiveModel::AttributeMethods
41
42
  include ActiveModel::Validations
42
43
  include ActiveModel::Conversion
43
44
  include ActiveModel::Dirty
@@ -52,10 +52,10 @@ module Her
52
52
  # Define an *has_many* association.
53
53
  #
54
54
  # @param [Symbol] name The name of the method added to resources
55
- # @param [Hash] attrs Options
56
- # @option attrs [String] :class_name The name of the class to map objects to
57
- # @option attrs [Symbol] :data_key The attribute where the data is stored
58
- # @option attrs [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
55
+ # @param [Hash] opts Options
56
+ # @option opts [String] :class_name The name of the class to map objects to
57
+ # @option opts [Symbol] :data_key The attribute where the data is stored
58
+ # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
59
59
  #
60
60
  # @example
61
61
  # class User
@@ -70,16 +70,17 @@ module Her
70
70
  # @user = User.find(1)
71
71
  # @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
72
72
  # # Fetched via GET "/users/1/articles"
73
- def has_many(name, attrs={})
74
- Her::Model::Associations::HasManyAssociation.attach(self, name, attrs)
73
+ def has_many(name, opts={})
74
+ Her::Model::Associations::HasManyAssociation.attach(self, name, opts)
75
75
  end
76
76
 
77
77
  # Define an *has_one* association.
78
78
  #
79
79
  # @param [Symbol] name The name of the method added to resources
80
- # @option attrs [String] :class_name The name of the class to map objects to
81
- # @option attrs [Symbol] :data_key The attribute where the data is stored
82
- # @option attrs [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
80
+ # @param [Hash] opts Options
81
+ # @option opts [String] :class_name The name of the class to map objects to
82
+ # @option opts [Symbol] :data_key The attribute where the data is stored
83
+ # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{name}`)
83
84
  #
84
85
  # @example
85
86
  # class User
@@ -94,17 +95,18 @@ module Her
94
95
  # @user = User.find(1)
95
96
  # @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
96
97
  # # Fetched via GET "/users/1/organization"
97
- def has_one(name, attrs={})
98
- Her::Model::Associations::HasOneAssociation.attach(self, name, attrs)
98
+ def has_one(name, opts={})
99
+ Her::Model::Associations::HasOneAssociation.attach(self, name, opts)
99
100
  end
100
101
 
101
102
  # Define a *belongs_to* association.
102
103
  #
103
104
  # @param [Symbol] name The name of the method added to resources
104
- # @option attrs [String] :class_name The name of the class to map objects to
105
- # @option attrs [Symbol] :data_key The attribute where the data is stored
106
- # @option attrs [Path] :path The relative path where to fetch the data (defaults to `/{class_name}.pluralize/{id}`)
107
- # @option attrs [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
105
+ # @param [Hash] opts Options
106
+ # @option opts [String] :class_name The name of the class to map objects to
107
+ # @option opts [Symbol] :data_key The attribute where the data is stored
108
+ # @option opts [Path] :path The relative path where to fetch the data (defaults to `/{class_name}.pluralize/{id}`)
109
+ # @option opts [Symbol] :foreign_key The foreign key used to build the `:id` part of the path (defaults to `{name}_id`)
108
110
  #
109
111
  # @example
110
112
  # class User
@@ -119,8 +121,8 @@ module Her
119
121
  # @user = User.find(1) # => #<User(users/1) id=1 team_id=2 name="Tobias">
120
122
  # @user.team # => #<Team(teams/2) id=2 name="Developers">
121
123
  # # Fetched via GET "/teams/2"
122
- def belongs_to(name, attrs={})
123
- Her::Model::Associations::BelongsToAssociation.attach(self, name, attrs)
124
+ def belongs_to(name, opts={})
125
+ Her::Model::Associations::BelongsToAssociation.attach(self, name, opts)
124
126
  end
125
127
  end
126
128
  end
@@ -2,22 +2,71 @@ module Her
2
2
  module Model
3
3
  module Associations
4
4
  class Association
5
- attr_accessor :query_attrs
5
+ # @private
6
+ attr_accessor :params
6
7
 
7
8
  # @private
8
9
  def initialize(parent, opts = {})
9
10
  @parent = parent
10
11
  @opts = opts
11
- @query_attrs = {}
12
+ @params = {}
12
13
 
13
14
  @klass = @parent.class.her_nearby_class(@opts[:class_name])
14
15
  @name = @opts[:name]
15
16
  end
16
17
 
18
+ # @private
19
+ def self.parse_single(association, klass, data)
20
+ data_key = association[:data_key]
21
+ return {} unless data[data_key]
22
+
23
+ klass = klass.her_nearby_class(association[:class_name])
24
+ { association[:name] => klass.new(data[data_key]) }
25
+ end
26
+
27
+ # @private
28
+ def assign_single_nested_attributes(attributes)
29
+ if @parent.attributes[@name].blank?
30
+ @parent.attributes[@name] = @klass.new(@klass.parse(attributes))
31
+ else
32
+ @parent.attributes[@name].assign_attributes(attributes)
33
+ end
34
+ end
35
+
36
+ # @private
37
+ def fetch(opts = {})
38
+ return @opts[:default].try(:dup) if @parent.attributes.include?(@name) && @parent.attributes[@name].empty? && @params.empty?
39
+
40
+ if @parent.attributes[@name].blank? || @params.any?
41
+ path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
42
+ @klass.get(path, @params)
43
+ else
44
+ @parent.attributes[@name]
45
+ end
46
+ end
47
+
48
+ # @private
49
+ def build_association_path(code)
50
+ begin
51
+ instance_exec(&code)
52
+ rescue Her::Errors::PathError
53
+ return nil
54
+ end
55
+ end
56
+
17
57
  # Add query parameters to the HTTP request performed to fetch the data
18
- def where(attrs = {})
19
- return self if attrs.blank?
20
- self.clone.tap { |a| a.query_attrs = a.query_attrs.merge(attrs) }
58
+ #
59
+ # @example
60
+ # class User
61
+ # include Her::Model
62
+ # has_many :comments
63
+ # end
64
+ #
65
+ # user = User.find(1)
66
+ # user.comments.where(:approved => 1) # Fetched via GET "/users/1/comments?approved=1
67
+ def where(params = {})
68
+ return self if params.blank? && @parent.attributes[@name].blank?
69
+ self.clone.tap { |a| a.params = a.params.merge(params) }
21
70
  end
22
71
  alias all where
23
72
 
@@ -3,33 +3,30 @@ module Her
3
3
  module Associations
4
4
  class BelongsToAssociation < Association
5
5
  # @private
6
- def self.attach(klass, name, attrs)
7
- attrs = {
6
+ def self.attach(klass, name, opts)
7
+ opts = {
8
8
  :class_name => name.to_s.classify,
9
9
  :name => name,
10
10
  :data_key => name,
11
+ :default => nil,
11
12
  :foreign_key => "#{name}_id",
12
13
  :path => "/#{name.to_s.pluralize}/:id"
13
- }.merge(attrs)
14
- klass.associations[:belongs_to] << attrs
14
+ }.merge(opts)
15
+ klass.associations[:belongs_to] << opts
15
16
 
16
17
  klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
18
  def #{name}
18
19
  cached_name = :"@_her_association_#{name}"
19
20
 
20
21
  cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
21
- cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, #{attrs.inspect}))
22
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, #{opts.inspect}))
22
23
  end
23
24
  RUBY
24
25
  end
25
26
 
26
27
  # @private
27
- def self.parse(association, klass, data)
28
- data_key = association[:data_key]
29
- return {} unless data[data_key]
30
-
31
- klass = klass.her_nearby_class(association[:class_name])
32
- { association[:name] => klass.new(data[data_key]) }
28
+ def self.parse(*args)
29
+ parse_single(*args)
33
30
  end
34
31
 
35
32
  # Initialize a new object
@@ -75,16 +72,12 @@ module Her
75
72
  # @private
76
73
  def fetch
77
74
  foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
78
- return nil if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @query_attrs.empty?) || (@parent.persisted? && foreign_key_value.blank?)
79
-
80
- if @parent.attributes[@name].blank? || @query_attrs.any?
81
- path = begin
82
- @klass.build_request_path(@parent.attributes.merge(@query_attrs.merge(@klass.primary_key => foreign_key_value)))
83
- rescue Her::Errors::PathError
84
- return nil
85
- end
75
+ return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (@parent.persisted? && foreign_key_value.blank?)
86
76
 
87
- @klass.get_resource(path, @query_attrs)
77
+ if @parent.attributes[@name].blank? || @params.any?
78
+ path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
79
+ path = build_association_path lambda { @klass.build_request_path(path_params) }
80
+ @klass.get(path, @params)
88
81
  else
89
82
  @parent.attributes[@name]
90
83
  end
@@ -92,11 +85,7 @@ module Her
92
85
 
93
86
  # @private
94
87
  def assign_nested_attributes(attributes)
95
- if @parent.attributes[@name].blank?
96
- @parent.attributes[@name] = @klass.new(@klass.parse(attributes))
97
- else
98
- @parent.attributes[@name].assign_attributes(attributes)
99
- end
88
+ assign_single_nested_attributes(attributes)
100
89
  end
101
90
  end
102
91
  end
@@ -3,22 +3,23 @@ module Her
3
3
  module Associations
4
4
  class HasManyAssociation < Association
5
5
  # @private
6
- def self.attach(klass, name, attrs)
7
- attrs = {
6
+ def self.attach(klass, name, opts)
7
+ opts = {
8
8
  :class_name => name.to_s.classify,
9
9
  :name => name,
10
10
  :data_key => name,
11
+ :default => Her::Collection.new,
11
12
  :path => "/#{name}",
12
13
  :inverse_of => nil
13
- }.merge(attrs)
14
- klass.associations[:has_many] << attrs
14
+ }.merge(opts)
15
+ klass.associations[:has_many] << opts
15
16
 
16
17
  klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
17
18
  def #{name}
18
19
  cached_name = :"@_her_association_#{name}"
19
20
 
20
21
  cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
21
- cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, #{attrs.inspect}))
22
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, #{opts.inspect}))
22
23
  end
23
24
  RUBY
24
25
  end
@@ -79,24 +80,10 @@ module Her
79
80
 
80
81
  # @private
81
82
  def fetch
82
- return Her::Collection.new if @parent.attributes.include?(@name) && @parent.attributes[@name].empty? && @query_attrs.empty?
83
-
84
- output = if @parent.attributes[@name].blank? || @query_attrs.any?
85
- path = begin
86
- @parent.request_path(@query_attrs)
87
- rescue Her::Errors::PathError
88
- return nil
89
- end
90
-
91
- @klass.get_collection("#{path}#{@opts[:path]}", @query_attrs)
92
- else
93
- @parent.attributes[@name]
83
+ super.tap do |o|
84
+ inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
85
+ o.each { |entry| entry.send("#{inverse_of}=", @parent) }
94
86
  end
95
-
96
- inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
97
- output.each { |entry| entry.send("#{inverse_of}=", @parent) }
98
-
99
- output
100
87
  end
101
88
 
102
89
  # @private