restful 0.2.20
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/CHANGES.markdown +40 -0
- data/LICENSE.markdown +22 -0
- data/README.markdown +126 -0
- data/Rakefile +22 -0
- data/TODO.markdown +10 -0
- data/init.rb +1 -0
- data/lib/restful.rb +65 -0
- data/lib/restful/apimodel/attribute.rb +17 -0
- data/lib/restful/apimodel/collection.rb +22 -0
- data/lib/restful/apimodel/link.rb +21 -0
- data/lib/restful/apimodel/map.rb +41 -0
- data/lib/restful/apimodel/resource.rb +23 -0
- data/lib/restful/converters/active_record.rb +160 -0
- data/lib/restful/rails.rb +22 -0
- data/lib/restful/rails/action_controller.rb +14 -0
- data/lib/restful/rails/active_record/configuration.rb +219 -0
- data/lib/restful/rails/active_record/metadata_tools.rb +102 -0
- data/lib/restful/serializers/atom_like_serializer.rb +51 -0
- data/lib/restful/serializers/base.rb +58 -0
- data/lib/restful/serializers/hash_serializer.rb +46 -0
- data/lib/restful/serializers/json_serializer.rb +18 -0
- data/lib/restful/serializers/params_serializer.rb +46 -0
- data/lib/restful/serializers/xml_serializer.rb +160 -0
- data/rails/init.rb +1 -0
- data/restful.gemspec +17 -0
- data/test/converters/active_record_converter_test.rb +147 -0
- data/test/converters/basic_types_converter_test.rb +99 -0
- data/test/fixtures/models/paginated_collection.rb +3 -0
- data/test/fixtures/models/person.rb +29 -0
- data/test/fixtures/models/pet.rb +5 -0
- data/test/fixtures/models/wallet.rb +5 -0
- data/test/fixtures/people.json.yaml +107 -0
- data/test/fixtures/people.xml.yaml +117 -0
- data/test/fixtures/pets.json.yaml +20 -0
- data/test/fixtures/pets.xml.yaml +31 -0
- data/test/rails/active_record_metadata_test.rb +23 -0
- data/test/rails/configuration_test.rb +47 -0
- data/test/rails/restful_publish_test.rb +54 -0
- data/test/serializers/atom_serializer_test.rb +33 -0
- data/test/serializers/json_serializer_test.rb +90 -0
- data/test/serializers/params_serializer_test.rb +76 -0
- data/test/serializers/xml_serializer_test.rb +51 -0
- data/test/test_helper.rb +154 -0
- metadata +106 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Restful
|
2
|
+
module Rails
|
3
|
+
|
4
|
+
# sets the hostname for this request in a threadsafe manner.
|
5
|
+
def self.api_hostname=(hostname)
|
6
|
+
Thread.current[:api_hostname] = hostname
|
7
|
+
end
|
8
|
+
|
9
|
+
# gets the hostname for the currently running thread.
|
10
|
+
def self.api_hostname
|
11
|
+
Thread.current[:api_hostname]
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Shortcuts past namespaces
|
16
|
+
#
|
17
|
+
def self.tools
|
18
|
+
Restful::Rails::ActiveRecord::MetadataTools::Utils
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
#
|
2
|
+
# Configuration options for restful.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module Rails
|
6
|
+
module ActiveRecord
|
7
|
+
module Utils
|
8
|
+
|
9
|
+
def self.get_includes(config)
|
10
|
+
if config && config.is_a?(Hash) && config.keys.size == 1 && includes = config[:include]
|
11
|
+
add_to_whitelist = [*includes].map { |el| el.is_a?(String) ? el.to_sym : el }
|
12
|
+
return nil, add_to_whitelist
|
13
|
+
else
|
14
|
+
return config, []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.convert_to_single_resource(config_parameter, el)
|
19
|
+
config = if config_parameter
|
20
|
+
returning Configuration::Config.new(config_parameter) do |c|
|
21
|
+
config, includes = Utils.get_includes(config_parameter)
|
22
|
+
c.whitelisted += el.class.restful_config.whitelisted unless config
|
23
|
+
c.whitelisted += includes unless includes.blank?
|
24
|
+
end
|
25
|
+
else
|
26
|
+
el.class.restful_config
|
27
|
+
end
|
28
|
+
|
29
|
+
Restful::Converters::ActiveRecord.convert(el, config)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Configuration
|
34
|
+
def self.included(base)
|
35
|
+
base.send :class_inheritable_accessor, :restful_config
|
36
|
+
base.restful_config = Config.new
|
37
|
+
base.send :include, ResourceInstanceMethods
|
38
|
+
base.send :include, CommonInstanceMethods
|
39
|
+
base.extend(ClassMethods)
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
|
44
|
+
#
|
45
|
+
# In the form:
|
46
|
+
#
|
47
|
+
# Person.restful_publish(:name, :pets => [:name, :species])
|
48
|
+
#
|
49
|
+
# If pet already has configured the api with restful_publish, you would
|
50
|
+
# get the default nested attributes. In the above example, these would be
|
51
|
+
# overriden.
|
52
|
+
#
|
53
|
+
def restful_publish(*fieldnames) # declarative setter method
|
54
|
+
self.restful_config = Restful.cfg(*fieldnames)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module CommonInstanceMethods
|
59
|
+
#
|
60
|
+
# converts this AR object to an apimodel object. per default, only the
|
61
|
+
# attributes in self.class.restful_config are shown. this can be overriden
|
62
|
+
# by passing in something like @pet.to_restful(:name, :species).
|
63
|
+
#
|
64
|
+
def to_restful(config_parameter = nil)
|
65
|
+
config, add_to_whitelist = Utils.get_includes(config_parameter)
|
66
|
+
|
67
|
+
config ||= self.class.restful_config.clone if self.class.respond_to?(:restful_config)
|
68
|
+
config ||= []
|
69
|
+
|
70
|
+
if config && !config.is_a?(Config)
|
71
|
+
config = Config.new(config)
|
72
|
+
end
|
73
|
+
|
74
|
+
if self.class.respond_to?(:restful_config)
|
75
|
+
config.whitelisted = Array.new(self.class.restful_config.whitelisted) if config.whitelisted.empty?
|
76
|
+
config.restful_options.merge! self.class.restful_config.restful_options
|
77
|
+
end
|
78
|
+
|
79
|
+
config.whitelisted += add_to_whitelist
|
80
|
+
config.whitelisted = config.whitelisted.uniq
|
81
|
+
|
82
|
+
# array
|
83
|
+
result = if self.is_a?(Array)
|
84
|
+
element_name = if fst = self.first
|
85
|
+
fst.class.respond_to?(:base_class) ?
|
86
|
+
fst.class.base_class.to_s.tableize :
|
87
|
+
fst.class.to_s.pluralize
|
88
|
+
elsif self.respond_to?(:name)
|
89
|
+
self.name
|
90
|
+
else
|
91
|
+
"nil-classes"
|
92
|
+
end
|
93
|
+
|
94
|
+
elements = self.map do |el|
|
95
|
+
raise TypeError.new("Not all array elements respond to #to_restful. ") unless el.respond_to?(:to_restful)
|
96
|
+
|
97
|
+
Utils.convert_to_single_resource(config_parameter, el)
|
98
|
+
end
|
99
|
+
|
100
|
+
returning Restful.collection(element_name, elements, :array) do |collection|
|
101
|
+
collection.total_entries = self.total_entries if self.respond_to?(:total_entries)
|
102
|
+
end
|
103
|
+
|
104
|
+
elsif self.is_a?(Hash)
|
105
|
+
|
106
|
+
elements = self.map do |k,v|
|
107
|
+
value = if v.respond_to?(:to_restful) and v.class.respond_to?(:restful_config)
|
108
|
+
Utils.convert_to_single_resource(config_parameter, v)
|
109
|
+
else
|
110
|
+
v.respond_to?(:to_restful) ? v.to_restful : v
|
111
|
+
end
|
112
|
+
Restful::ApiModel::Attribute.new(k, value, :map)
|
113
|
+
end
|
114
|
+
|
115
|
+
map = Restful::ApiModel::Map.new("hash")
|
116
|
+
map.values = elements
|
117
|
+
map
|
118
|
+
else
|
119
|
+
Restful::Converters::ActiveRecord.convert(self, config)
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
# FIXME: read Restful::Serializers::Base.serializers. Load order problems?
|
126
|
+
[:atom_like, :hash, :json, :params, :xml].each do |format|
|
127
|
+
define_method("to_restful_#{ format }") do |*args|
|
128
|
+
self.to_restful(*args).serialize(format)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
module ResourceInstanceMethods
|
134
|
+
|
135
|
+
# simple method through which a model should know it's own name. override this where necessary.
|
136
|
+
def restful_url(url_base = Restful::Rails.api_hostname)
|
137
|
+
"#{ url_base }#{ restful_path }"
|
138
|
+
end
|
139
|
+
|
140
|
+
def restful_path
|
141
|
+
"/#{ self.class.base_class.to_s.tableize }/#{ self.to_param }"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Config # configures what attributes are exposed to the api. for a single resource.
|
146
|
+
|
147
|
+
attr_accessor :whitelisted, :restful_options
|
148
|
+
|
149
|
+
def initialize(*fields) # set; eg :name, :pets => [:name, :species]
|
150
|
+
@whitelisted, @restful_options = split_into_whitelist_and_restful_options([fields].flatten.compact)
|
151
|
+
end
|
152
|
+
|
153
|
+
def published?(key)
|
154
|
+
@whitelisted.include?(key) ||
|
155
|
+
!!@whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first ||
|
156
|
+
explicit_include?(key)
|
157
|
+
end
|
158
|
+
|
159
|
+
def expanded?(key, nested = false) # if nothing was set, this defaults to true.
|
160
|
+
force_expanded?(key) ||
|
161
|
+
(@restful_options[:expansion] != :collapsed && !nested) ||
|
162
|
+
explicit_include?(key)
|
163
|
+
end
|
164
|
+
|
165
|
+
def explicit_include?(key)
|
166
|
+
[*self.restful_options[:include]].include?(key)
|
167
|
+
end
|
168
|
+
|
169
|
+
def force_expanded?(key)
|
170
|
+
force = [*@restful_options[:force_expand]]
|
171
|
+
force.include?(key)
|
172
|
+
end
|
173
|
+
|
174
|
+
def nested?
|
175
|
+
!!restful_options[:nested]
|
176
|
+
end
|
177
|
+
|
178
|
+
def nested(key)
|
179
|
+
definition = @whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first
|
180
|
+
config = Config.new((definition[key] if definition))
|
181
|
+
config.restful_options[:include] = self.restful_options[:include]
|
182
|
+
config
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def split_into_whitelist_and_restful_options(array)
|
188
|
+
options = {}
|
189
|
+
|
190
|
+
return array.map do |el|
|
191
|
+
if el.is_a? Hash
|
192
|
+
el = el.clone
|
193
|
+
|
194
|
+
deleted = {}
|
195
|
+
if restful_options = el.delete(:restful_options)
|
196
|
+
deleted.merge! restful_options
|
197
|
+
end
|
198
|
+
|
199
|
+
if included = el.delete(:include)
|
200
|
+
deleted[:include] = included
|
201
|
+
end
|
202
|
+
|
203
|
+
options.merge!(deleted) if deleted.keys.size > 0
|
204
|
+
el = nil if el == {}
|
205
|
+
end
|
206
|
+
|
207
|
+
el
|
208
|
+
end.compact, options
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
ActiveRecord::Base.send :include, Restful::Rails::ActiveRecord::Configuration
|
218
|
+
Array.send :include, Restful::Rails::ActiveRecord::Configuration::CommonInstanceMethods
|
219
|
+
Hash.send :include, Restful::Rails::ActiveRecord::Configuration::CommonInstanceMethods
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#
|
2
|
+
# Handle ActiveRecord associations and such like.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module Rails
|
6
|
+
module ActiveRecord
|
7
|
+
module MetadataTools
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_inheritable_accessor :apiable_associations
|
11
|
+
base.class_inheritable_accessor :apiable_association_table
|
12
|
+
|
13
|
+
base.send :include, InstanceMethods
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def apiable
|
20
|
+
cache_association_restful_url_metadata
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_association_restful_url_metadata
|
24
|
+
self.apiable_associations ||= (self.reflect_on_all_associations(:belongs_to) + self.reflect_on_all_associations(:has_one)).flatten.uniq
|
25
|
+
self.apiable_association_table ||= self.apiable_associations.inject({}) { |memo, reflection| memo[reflection.primary_key_name] = reflection; memo }
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_by_restful(id)
|
29
|
+
find(id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
def resolve_association_restful_url(association_key_name)
|
35
|
+
self.class.cache_association_restful_url_metadata
|
36
|
+
|
37
|
+
if reflection = self.class.apiable_association_table[association_key_name]
|
38
|
+
related_resource = self.send(reflection.name)
|
39
|
+
[Restful::Rails.api_hostname, related_resource.restful_path] if related_resource
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Utils
|
45
|
+
|
46
|
+
# called for nested resources.
|
47
|
+
def self.expand(resource, config)
|
48
|
+
config.restful_options[:nested] = true
|
49
|
+
resource.to_restful(config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.dereference(url)
|
53
|
+
if url
|
54
|
+
regexp = Regexp.new("#{ Restful::Rails.api_hostname }\/(.*)\/(.*)")
|
55
|
+
m, resource, params = *url.match(regexp)
|
56
|
+
resource = if resource && params
|
57
|
+
clazz = resource.try(:singularize).try(:camelize).try(:constantize)
|
58
|
+
clazz.find_by_restful(params) if clazz
|
59
|
+
end
|
60
|
+
|
61
|
+
resource.id if resource
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# retruns non association / collection attributes.
|
66
|
+
def self.simple_attributes_on(model)
|
67
|
+
attributes = model.attributes
|
68
|
+
|
69
|
+
attributes.delete_if do |k, v|
|
70
|
+
model.class.apiable_association_table && model.class.apiable_association_table.keys.include?(k)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Takes an ar model and a key like :people, and returns an array of resources.
|
76
|
+
#
|
77
|
+
# TODO: don't load the entire association, only the published attributes (with an appropriate :select).
|
78
|
+
# TODO: get some pagination in.
|
79
|
+
#
|
80
|
+
def self.convert_collection_to_resources(model, key, config)
|
81
|
+
|
82
|
+
# load the associated objects.
|
83
|
+
models = model.send(key)
|
84
|
+
|
85
|
+
# convert them to_restful.
|
86
|
+
if models
|
87
|
+
[*models].map do |m|
|
88
|
+
if m.respond_to? :to_restful
|
89
|
+
expand(m, config)
|
90
|
+
else
|
91
|
+
raise "Seems as if you want to export the relation #{ key } of an #{ model.class.to_s } object without making #{ key } apiable."
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
ActiveRecord::Base.send :include, Restful::Rails::ActiveRecord::MetadataTools
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'restful/serializers/base'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Converts an APIModel to and from XML.
|
6
|
+
#
|
7
|
+
module Restful
|
8
|
+
module Serializers
|
9
|
+
class AtomLikeSerializer < XMLSerializer
|
10
|
+
|
11
|
+
serializer_name :atom_like
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def root_resource(node)
|
16
|
+
url_base = node.attribute(:base, :xml)
|
17
|
+
me_node = node.delete_element("link[@rel='self']")
|
18
|
+
own_url = me_node.attribute(:href)
|
19
|
+
Restful.resource(node.name, :path => own_url, :base => url_base)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_link(el, type)
|
23
|
+
Restful.link(revert_link_name(el.attribute('rel')), nil, el.attribute('href'), type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def calculate_node_type(el)
|
27
|
+
return :link if el.name.downcase == "link"
|
28
|
+
(el.attributes["type"] || "string").to_sym
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_link_to(resource, builder, options = {})
|
32
|
+
is_self = !!options[:self]
|
33
|
+
builder.tag!("link", { :href => resource.path, :rel => (is_self ? "self" : resource.name) })
|
34
|
+
end
|
35
|
+
|
36
|
+
def root_element(resource)
|
37
|
+
decorations = {}
|
38
|
+
|
39
|
+
unless @nested_root
|
40
|
+
decorations = { :"xml:base" => Restful::Rails.api_hostname } unless Restful::Rails.api_hostname.blank?
|
41
|
+
@nested_root = true
|
42
|
+
end
|
43
|
+
|
44
|
+
[resource.name, decorations]
|
45
|
+
end
|
46
|
+
|
47
|
+
def decorations(value); {}; end
|
48
|
+
def collections_decorations; {}; end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#
|
2
|
+
# Converts an APIModel to and from a given format.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module Serializers
|
6
|
+
class Base
|
7
|
+
cattr_accessor :serializers
|
8
|
+
|
9
|
+
def serialize(resource, options = {})
|
10
|
+
raise NotImplementedError.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(resource, options = {})
|
14
|
+
raise NotImplementedError.new
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Grabs a serializer, given...
|
19
|
+
#
|
20
|
+
# #serialize :xml, Resource.new(:animal => "cow")
|
21
|
+
#
|
22
|
+
def self.serializer(type)
|
23
|
+
serializers[type].new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.serializer_name(key)
|
27
|
+
self.serializers ||= {}
|
28
|
+
self.serializers[key] = self
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize_attribute(attribute)
|
32
|
+
return formatted_ruby_type(attribute) unless attribute.respond_to?(:value)
|
33
|
+
return nil if attribute.value.blank? && attribute.extended_type != :false_class
|
34
|
+
|
35
|
+
formatted_ruby_type(attribute.value)
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_ruby_type(obj)
|
39
|
+
case obj
|
40
|
+
when DateTime then obj.xmlschema
|
41
|
+
when Time then obj.xmlschema
|
42
|
+
when Date then obj.to_s(:db)
|
43
|
+
else
|
44
|
+
obj
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
def transform_link_name(name)
|
50
|
+
name.to_s.gsub /_id$/, "-restful-url"
|
51
|
+
end
|
52
|
+
|
53
|
+
def revert_link_name(name)
|
54
|
+
name.to_s.gsub /-restful-url$/, "_id"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|