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.
Files changed (44) hide show
  1. data/CHANGES.markdown +40 -0
  2. data/LICENSE.markdown +22 -0
  3. data/README.markdown +126 -0
  4. data/Rakefile +22 -0
  5. data/TODO.markdown +10 -0
  6. data/init.rb +1 -0
  7. data/lib/restful.rb +65 -0
  8. data/lib/restful/apimodel/attribute.rb +17 -0
  9. data/lib/restful/apimodel/collection.rb +22 -0
  10. data/lib/restful/apimodel/link.rb +21 -0
  11. data/lib/restful/apimodel/map.rb +41 -0
  12. data/lib/restful/apimodel/resource.rb +23 -0
  13. data/lib/restful/converters/active_record.rb +160 -0
  14. data/lib/restful/rails.rb +22 -0
  15. data/lib/restful/rails/action_controller.rb +14 -0
  16. data/lib/restful/rails/active_record/configuration.rb +219 -0
  17. data/lib/restful/rails/active_record/metadata_tools.rb +102 -0
  18. data/lib/restful/serializers/atom_like_serializer.rb +51 -0
  19. data/lib/restful/serializers/base.rb +58 -0
  20. data/lib/restful/serializers/hash_serializer.rb +46 -0
  21. data/lib/restful/serializers/json_serializer.rb +18 -0
  22. data/lib/restful/serializers/params_serializer.rb +46 -0
  23. data/lib/restful/serializers/xml_serializer.rb +160 -0
  24. data/rails/init.rb +1 -0
  25. data/restful.gemspec +17 -0
  26. data/test/converters/active_record_converter_test.rb +147 -0
  27. data/test/converters/basic_types_converter_test.rb +99 -0
  28. data/test/fixtures/models/paginated_collection.rb +3 -0
  29. data/test/fixtures/models/person.rb +29 -0
  30. data/test/fixtures/models/pet.rb +5 -0
  31. data/test/fixtures/models/wallet.rb +5 -0
  32. data/test/fixtures/people.json.yaml +107 -0
  33. data/test/fixtures/people.xml.yaml +117 -0
  34. data/test/fixtures/pets.json.yaml +20 -0
  35. data/test/fixtures/pets.xml.yaml +31 -0
  36. data/test/rails/active_record_metadata_test.rb +23 -0
  37. data/test/rails/configuration_test.rb +47 -0
  38. data/test/rails/restful_publish_test.rb +54 -0
  39. data/test/serializers/atom_serializer_test.rb +33 -0
  40. data/test/serializers/json_serializer_test.rb +90 -0
  41. data/test/serializers/params_serializer_test.rb +76 -0
  42. data/test/serializers/xml_serializer_test.rb +51 -0
  43. data/test/test_helper.rb +154 -0
  44. 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,14 @@
1
+ module Restful
2
+ module Rails
3
+ module ActionController
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ ActionController::Base.send :include, Restful::Rails::ActionController
@@ -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