extended_her 0.5
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.
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +2 -0
- data/LICENSE +7 -0
- data/README.md +723 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +32 -0
- data/examples/twitter-oauth/Gemfile +13 -0
- data/examples/twitter-oauth/app.rb +50 -0
- data/examples/twitter-oauth/config.ru +5 -0
- data/examples/twitter-oauth/views/index.haml +9 -0
- data/examples/twitter-search/Gemfile +12 -0
- data/examples/twitter-search/app.rb +55 -0
- data/examples/twitter-search/config.ru +5 -0
- data/examples/twitter-search/views/index.haml +9 -0
- data/extended_her.gemspec +27 -0
- data/lib/her.rb +23 -0
- data/lib/her/api.rb +108 -0
- data/lib/her/base.rb +17 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +5 -0
- data/lib/her/exceptions/exception.rb +4 -0
- data/lib/her/exceptions/record_invalid.rb +8 -0
- data/lib/her/exceptions/record_not_found.rb +13 -0
- data/lib/her/middleware.rb +9 -0
- data/lib/her/middleware/accept_json.rb +15 -0
- data/lib/her/middleware/first_level_parse_json.rb +34 -0
- data/lib/her/middleware/second_level_parse_json.rb +28 -0
- data/lib/her/model.rb +69 -0
- data/lib/her/model/base.rb +7 -0
- data/lib/her/model/hooks.rb +114 -0
- data/lib/her/model/http.rb +284 -0
- data/lib/her/model/introspection.rb +57 -0
- data/lib/her/model/orm.rb +191 -0
- data/lib/her/model/orm/comparison_methods.rb +20 -0
- data/lib/her/model/orm/create_methods.rb +29 -0
- data/lib/her/model/orm/destroy_methods.rb +53 -0
- data/lib/her/model/orm/error_methods.rb +19 -0
- data/lib/her/model/orm/fields_definition.rb +15 -0
- data/lib/her/model/orm/find_methods.rb +46 -0
- data/lib/her/model/orm/persistance_methods.rb +22 -0
- data/lib/her/model/orm/relation_mapper.rb +21 -0
- data/lib/her/model/orm/save_methods.rb +58 -0
- data/lib/her/model/orm/serialization_methods.rb +28 -0
- data/lib/her/model/orm/update_methods.rb +31 -0
- data/lib/her/model/paths.rb +82 -0
- data/lib/her/model/relationships.rb +191 -0
- data/lib/her/paginated_collection.rb +20 -0
- data/lib/her/relation.rb +94 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +42 -0
- data/spec/middleware/second_level_parse_json_spec.rb +25 -0
- data/spec/model/hooks_spec.rb +406 -0
- data/spec/model/http_spec.rb +184 -0
- data/spec/model/introspection_spec.rb +59 -0
- data/spec/model/orm_spec.rb +552 -0
- data/spec/model/paths_spec.rb +286 -0
- data/spec/model/relationships_spec.rb +222 -0
- data/spec/model_spec.rb +31 -0
- data/spec/spec_helper.rb +46 -0
- metadata +222 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module Introspection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
# Inspect an element, returns it for introspection.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class User
|
9
|
+
# include Her::Model
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# @user = User.find(1)
|
13
|
+
# p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
|
14
|
+
def inspect
|
15
|
+
"#<#{self.class} #{@data.keys.map { |k| "#{k}: #{attribute_for_inspect(send(k))}" }.join(", ")}>"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
# @private
|
20
|
+
def attribute_for_inspect(value)
|
21
|
+
if value.is_a?(String) && value.length > 50
|
22
|
+
"#{value[0..50]}...".inspect
|
23
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
24
|
+
%("#{value}")
|
25
|
+
else
|
26
|
+
value.inspect
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
# Finds a class at the same level as this one or at the global level.
|
32
|
+
# @private
|
33
|
+
def nearby_class(name)
|
34
|
+
sibling_class(name) || name.constantize rescue nil
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
# Looks for a class at the same level as this one with the given name.
|
39
|
+
# @private
|
40
|
+
def sibling_class(name)
|
41
|
+
if mod = self.containing_module
|
42
|
+
@sibling_class ||= {}
|
43
|
+
@sibling_class[mod] ||= {}
|
44
|
+
@sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# If available, returns the containing Module for this class.
|
49
|
+
# @private
|
50
|
+
def containing_module
|
51
|
+
return unless self.name =~ /::/
|
52
|
+
self.name.split("::")[0..-2].join("::").constantize
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
# This module adds ORM-like capabilities to the model
|
4
|
+
module ORM
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include CreateMethods
|
7
|
+
include DestroyMethods
|
8
|
+
include FindMethods
|
9
|
+
include SaveMethods
|
10
|
+
include UpdateMethods
|
11
|
+
include RelationMapper
|
12
|
+
include ErrorMethods
|
13
|
+
include ComparisonMethods
|
14
|
+
include PersistanceMethods
|
15
|
+
include SerializationMethods
|
16
|
+
include FieldsDefinition
|
17
|
+
|
18
|
+
attr_accessor :data, :metadata, :errors
|
19
|
+
alias :attributes :data
|
20
|
+
alias :attributes= :data=
|
21
|
+
|
22
|
+
# Initialize a new object with data received from an HTTP request
|
23
|
+
def initialize(params={})
|
24
|
+
fields = self.class.instance_variable_defined?(:@fields) ? self.class.instance_variable_get(:@fields) : []
|
25
|
+
@data = Hash[fields.map{ |field| [field, nil] }]
|
26
|
+
@metadata = params.delete(:_metadata) || {}
|
27
|
+
@errors = params.delete(:_errors) || {}
|
28
|
+
|
29
|
+
# Use setter methods first, then translate attributes of relationships
|
30
|
+
# into relationship instances, then merge the parsed_data into @data.
|
31
|
+
unset_data = Her::Model::ORM.use_setter_methods(self, params)
|
32
|
+
parsed_data = self.class.parse_relationships(unset_data)
|
33
|
+
@data.update(convert_types(parsed_data))
|
34
|
+
end
|
35
|
+
|
36
|
+
def convert_types(parsed_data)
|
37
|
+
parsed_data.each do |key, value|
|
38
|
+
"2013-01-26T09:29:39.358Z"
|
39
|
+
if value.is_a?(String)
|
40
|
+
m = value.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z/)
|
41
|
+
next if m.nil?
|
42
|
+
if m[1].nil?
|
43
|
+
parsed_data[key] = Time.strptime(value, '%Y-%m-%dT%H:%M:%S%Z')
|
44
|
+
else
|
45
|
+
parsed_data[key] = Time.strptime(value, '%Y-%m-%dT%H:%M:%S.%L%Z')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Initialize a collection of resources
|
52
|
+
# @private
|
53
|
+
def self.initialize_collection(klass, parsed_data={})
|
54
|
+
collection_data = parsed_data[:data].map do |item_data|
|
55
|
+
resource = klass.new(klass.parse(item_data))
|
56
|
+
klass.wrap_in_hooks(resource, :find)
|
57
|
+
resource
|
58
|
+
end
|
59
|
+
|
60
|
+
collection_class = Her::Collection
|
61
|
+
if parsed_data[:metadata].is_a?(Hash) && parsed_data[:metadata].has_key?(:current_page)
|
62
|
+
collection_class = Her::PaginatedCollection
|
63
|
+
end
|
64
|
+
collection_class.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Handles missing methods by routing them through @data
|
68
|
+
# @private
|
69
|
+
def method_missing(method, *args, &blk)
|
70
|
+
if method.to_s.end_with?('=')
|
71
|
+
@data[method.to_s.chomp('=').to_sym] = args.first
|
72
|
+
elsif method.to_s.end_with?('?')
|
73
|
+
@data.include?(method.to_s.chomp('?').to_sym)
|
74
|
+
elsif @data.include?(method)
|
75
|
+
@data[method]
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Handles returning true for the cases handled by method_missing
|
82
|
+
def respond_to?(method, include_private = false)
|
83
|
+
method.to_s.end_with?('=') || method.to_s.end_with?('?') || @data.include?(method) || super
|
84
|
+
end
|
85
|
+
|
86
|
+
# Assign new data to an instance
|
87
|
+
def assign_data(new_data)
|
88
|
+
new_data = Her::Model::ORM.use_setter_methods(self, new_data)
|
89
|
+
@data.update new_data
|
90
|
+
end
|
91
|
+
alias :assign_attributes :assign_data
|
92
|
+
|
93
|
+
# Handles returning true for the accessible attributes
|
94
|
+
def has_data?(attribute_name)
|
95
|
+
@data.include?(attribute_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Handles returning attribute value from data
|
99
|
+
def get_data(attribute_name)
|
100
|
+
@data[attribute_name]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Override the method to prevent from returning the object ID (in ruby-1.8.7)
|
104
|
+
# @private
|
105
|
+
def id
|
106
|
+
@data[:id] || super
|
107
|
+
end
|
108
|
+
|
109
|
+
# Use setter methods of model for each key / value pair in params
|
110
|
+
# Return key / value pairs for which no setter method was defined on the model
|
111
|
+
# @private
|
112
|
+
def self.use_setter_methods(model, params)
|
113
|
+
setter_method_names = model.class.setter_method_names
|
114
|
+
params.inject({}) do |memo, (key, value)|
|
115
|
+
setter_method = key.to_s + '='
|
116
|
+
if setter_method_names.include?(setter_method)
|
117
|
+
model.send(setter_method, value)
|
118
|
+
else
|
119
|
+
if key.is_a?(String)
|
120
|
+
key = key.to_sym
|
121
|
+
end
|
122
|
+
memo[key] = value
|
123
|
+
end
|
124
|
+
memo
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module ClassMethods
|
129
|
+
# Initialize a collection of resources with raw data from an HTTP request
|
130
|
+
#
|
131
|
+
# @param [Array] parsed_data
|
132
|
+
def new_collection(parsed_data)
|
133
|
+
Her::Model::ORM.initialize_collection(self, parsed_data)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Parse data before assigning it to a resource
|
137
|
+
#
|
138
|
+
# @param [Hash] data
|
139
|
+
def parse(data)
|
140
|
+
if parse_root_in_json
|
141
|
+
parse_root_in_json == true ? data[root_element.to_sym] : data[parse_root_in_json]
|
142
|
+
else
|
143
|
+
data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Save an existing resource and return it
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# @user = User.save_existing(1, { fullname: "Tobias Fünke" })
|
151
|
+
# # Called via PUT "/users/1"
|
152
|
+
def save_existing(id, params)
|
153
|
+
resource = new(params.merge(id: id))
|
154
|
+
resource.save
|
155
|
+
resource
|
156
|
+
end
|
157
|
+
|
158
|
+
# Destroy an existing resource
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# User.destroy_existing(1)
|
162
|
+
# # Called via DELETE "/users/1"
|
163
|
+
def destroy_existing(id, params={})
|
164
|
+
request(params.merge(_method: :delete, _path: build_request_path(params.merge(id: id)))) do |parsed_data|
|
165
|
+
new(parse(parsed_data[:data]))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @private
|
170
|
+
def setter_method_names
|
171
|
+
@setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
|
172
|
+
memo << method_name.to_s if method_name.to_s.end_with?('=')
|
173
|
+
memo
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Return or change the value of `include_root_in_json`
|
178
|
+
def include_root_in_json(value=nil)
|
179
|
+
return @include_root_in_json if value.nil?
|
180
|
+
@include_root_in_json = value
|
181
|
+
end
|
182
|
+
|
183
|
+
# Return or change the value of `parse_root_in`
|
184
|
+
def parse_root_in_json(value=nil)
|
185
|
+
return @parse_root_in_json if value.nil?
|
186
|
+
@parse_root_in_json = value
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module ComparisonMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Return `true` if the other object is also a Her::Model and has matching data
|
8
|
+
def ==(other)
|
9
|
+
other.is_a?(Her::Model) && @data == other.data
|
10
|
+
end
|
11
|
+
|
12
|
+
# Delegate to the == method
|
13
|
+
def eql?(other)
|
14
|
+
self == other
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module CreateMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Create a resource and return it
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# @user = User.create({ fullname: "Tobias Fünke" })
|
12
|
+
# # Called via POST "/users/1"
|
13
|
+
def create(params = {})
|
14
|
+
resource = new(params)
|
15
|
+
resource.save
|
16
|
+
resource
|
17
|
+
end
|
18
|
+
|
19
|
+
def create!(params = {})
|
20
|
+
resource = create(params)
|
21
|
+
raise RecordInvalid.new(resource.errors) unless resource.errors.empty?
|
22
|
+
|
23
|
+
resource
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module DestroyMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Destroy a resource
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# @user = User.find(1)
|
11
|
+
# @user.destroy
|
12
|
+
# # Called via DELETE "/users/1"
|
13
|
+
def destroy
|
14
|
+
resource = self
|
15
|
+
self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
|
16
|
+
klass.request(_method: :delete, _path: request_path) do |parsed_data|
|
17
|
+
self.data = self.class.parse(parsed_data[:data])
|
18
|
+
self.metadata = parsed_data[:metadata]
|
19
|
+
self.errors = parsed_data[:errors]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete
|
26
|
+
resource = self
|
27
|
+
self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
|
28
|
+
klass.request({_method: :delete, _path: build_request_path(params.merge(soft: true))}) do |parsed_data|
|
29
|
+
self.data = self.class.parse(parsed_data[:data])
|
30
|
+
self.metadata = parsed_data[:metadata]
|
31
|
+
self.errors = parsed_data[:errors]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
def destroy(ids)
|
39
|
+
is_array = ids.is_a?(Array)
|
40
|
+
ids = Array(ids)
|
41
|
+
collection = ids.map{ |id| new(id: id).destroy }
|
42
|
+
collection = collection.first unless is_array
|
43
|
+
collection
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(ids)
|
47
|
+
Array(ids).map{ |id| new(id: id).delete }.count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module ErrorMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Return `true` if a resource does not contain errors
|
8
|
+
def valid?
|
9
|
+
errors.empty?
|
10
|
+
end
|
11
|
+
|
12
|
+
# Return `true` if a resource contains errors
|
13
|
+
def invalid?
|
14
|
+
errors.any?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module FindMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Fetch specific resource(s) by their ID
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# @user = User.find(1)
|
12
|
+
# # Fetched via GET "/users/1"
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# @users = User.find([1, 2])
|
16
|
+
# # Fetched via GET "/users/1" and GET "/users/2"
|
17
|
+
def find(*ids)
|
18
|
+
params = ids.last.is_a?(Hash) ? ids.pop : {}
|
19
|
+
prepared_ids = ids.flatten.compact.uniq
|
20
|
+
results = prepared_ids.map do |id|
|
21
|
+
resource = nil
|
22
|
+
request(params.merge(_method: :get, _path: build_request_path(params.merge(id: id)))) do |parsed_data|
|
23
|
+
resource = new(parse(parsed_data[:data]).merge _metadata: parsed_data[:data], _errors: parsed_data[:errors])
|
24
|
+
wrap_in_hooks(resource, :find)
|
25
|
+
end
|
26
|
+
resource
|
27
|
+
end
|
28
|
+
if ids.length > 1 || ids.first.kind_of?(Array)
|
29
|
+
raise RecordNotFound.one(self.class, ids) if results.nil?
|
30
|
+
results
|
31
|
+
else
|
32
|
+
raise RecordNotFound.some(self.class, ids, results.length, prepared_ids.length) if results.length != prepared_ids.length
|
33
|
+
results.first
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_by_id(id)
|
38
|
+
find(id)
|
39
|
+
rescue RecordNotFound
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Her
|
2
|
+
module Model
|
3
|
+
module ORM
|
4
|
+
module PersistanceMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def new?
|
8
|
+
!@data.include?(:id)
|
9
|
+
end
|
10
|
+
alias_method :new_record?, :new?
|
11
|
+
|
12
|
+
def persisted?
|
13
|
+
!new_record?
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroyed?
|
17
|
+
@metadata.include?(:destroyed) && @metadata[:destroyed] == true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|