restful 0.2.20

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