her 0.6.3 → 0.6.4

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