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