benjaminkrause-restful 0.2.8

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 +8 -0
  2. data/LICENSE.markdown +22 -0
  3. data/README.markdown +123 -0
  4. data/Rakefile +22 -0
  5. data/TODO.markdown +9 -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 +131 -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 +167 -0
  17. data/lib/restful/rails/active_record/metadata_tools.rb +106 -0
  18. data/lib/restful/serializers/atom_like_serializer.rb +51 -0
  19. data/lib/restful/serializers/base.rb +57 -0
  20. data/lib/restful/serializers/hash_serializer.rb +59 -0
  21. data/lib/restful/serializers/json_serializer.rb +20 -0
  22. data/lib/restful/serializers/params_serializer.rb +46 -0
  23. data/lib/restful/serializers/xml_serializer.rb +161 -0
  24. data/rails/init.rb +1 -0
  25. data/restful.gemspec +17 -0
  26. data/test/converters/active_record_converter_test.rb +122 -0
  27. data/test/converters/basic_types_converter_test.rb +48 -0
  28. data/test/fixtures/models/paginated_collection.rb +4 -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 +94 -0
  33. data/test/fixtures/people.xml.yaml +123 -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 +40 -0
  38. data/test/rails/restful_publish_test.rb +52 -0
  39. data/test/serializers/atom_serializer_test.rb +33 -0
  40. data/test/serializers/json_serializer_test.rb +82 -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 +147 -0
  44. metadata +98 -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,167 @@
1
+ #
2
+ # Configuration options for restful.
3
+ #
4
+ module Restful
5
+ module Rails
6
+ module ActiveRecord
7
+ module Configuration
8
+ def self.included(base)
9
+ base.send :class_inheritable_accessor, :restful_config
10
+ base.restful_config = Config.new
11
+ base.send :include, ResourceInstanceMethods
12
+ base.send :include, CommonInstanceMethods
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ #
19
+ # In the form:
20
+ #
21
+ # Person.restful_publish(:name, :pets => [:name, :species])
22
+ #
23
+ # If pet already has configured the api with restful_publish, you would
24
+ # get the default nested attributes. In the above example, these would be
25
+ # overriden.
26
+ #
27
+ def restful_publish(*fieldnames) # declarative setter method
28
+ self.restful_config = Restful.cfg(*fieldnames)
29
+ end
30
+ end
31
+
32
+ module CommonInstanceMethods
33
+ #
34
+ # converts this AR object to an apimodel object. per default, only the
35
+ # attributes in self.class.restful_config are shown. this can be overriden
36
+ # by passing in something like @pet.to_restful(:name, :species).
37
+ #
38
+ def to_restful(config = nil)
39
+ add_to_whitelist = []
40
+
41
+ if config && config.is_a?(Hash) && config.keys.size == 1 && includes = config[:include]
42
+ add_to_whitelist = [*includes].map { |el| el.is_a?(String) ? el.to_sym : el }
43
+ config = nil
44
+ end
45
+
46
+ config ||= self.class.restful_config.clone if self.class.respond_to?(:restful_config)
47
+ config ||= []
48
+
49
+ if config && !config.is_a?(Config)
50
+ config = Config.new(config)
51
+ end
52
+
53
+ if self.class.respond_to?(:restful_config)
54
+ config.whitelisted = Array.new(self.class.restful_config.whitelisted) if config.whitelisted.empty?
55
+ config.restful_options.merge! self.class.restful_config.restful_options
56
+ end
57
+
58
+ config.whitelisted += add_to_whitelist
59
+ config.whitelisted = config.whitelisted.uniq
60
+
61
+ # array
62
+ if self.is_a?(Array)
63
+ elements = self.map do |el|
64
+ raise TypeError.new("Not all array elements respond to #to_restful. ") unless el.respond_to?(:to_restful)
65
+ Restful::Converters::ActiveRecord.convert(el, el.class.restful_config)
66
+ end
67
+
68
+ element_name = elements.first ? elements.first.name.pluralize : "nil-classes"
69
+
70
+ returning Restful.collection(element_name, elements, :array) do |collection|
71
+ collection.total_entries = self.total_entries if self.respond_to?(:total_entries)
72
+ end
73
+
74
+ elsif self.is_a?(Hash)
75
+ elements = self.map do |k,v|
76
+ if v.respond_to?(:to_restful) and v.class.respond_to?(:restful_config)
77
+ value = Restful::Converters::ActiveRecord.convert(v, v.class.restful_config)
78
+ else
79
+ value = v.respond_to?(:to_restful) ? v.to_restful : v
80
+ end
81
+ Restful::ApiModel::Attribute.new(k, value, :map)
82
+ end
83
+
84
+ map = Restful::ApiModel::Map.new("hash")
85
+ map.values = elements
86
+ map
87
+ else
88
+ Restful::Converters::ActiveRecord.convert(self, config)
89
+ end
90
+ end
91
+
92
+ # FIXME: read Restful::Serializers::Base.serializers. Load order problems?
93
+ [:xml, :json, :atom_like].each do |format|
94
+ define_method("to_restful_#{ format }") do |*args|
95
+ self.to_restful(*args).serialize(format)
96
+ end
97
+ end
98
+ end
99
+
100
+ module ResourceInstanceMethods
101
+
102
+ # simple method through which a model should know it's own name. override this where necessary.
103
+ def restful_url(url_base = Restful::Rails.api_hostname)
104
+ "#{ url_base }#{ restful_path }"
105
+ end
106
+
107
+ def restful_path
108
+ "/#{ self.class.to_s.tableize }/#{ self.to_param }"
109
+ end
110
+ end
111
+
112
+ class Config # configures what attributes are exposed to the api. for a single resource.
113
+
114
+ attr_accessor :whitelisted, :restful_options
115
+
116
+ def initialize(*fields) # set; eg :name, :pets => [:name, :species]
117
+ @whitelisted, @restful_options = split_into_whitelist_and_restful_options([fields].flatten.compact)
118
+ end
119
+
120
+ def published?(key)
121
+ @whitelisted.include?(key) || !!@whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first
122
+ end
123
+
124
+ def expanded?(key, nested = false) # if nothing was set, this defaults to true.
125
+ force_expanded?(key) || (@restful_options[:expansion] != :collapsed && !nested)
126
+ end
127
+
128
+ def force_expanded?(key)
129
+ force = [*@restful_options[:force_expand]]
130
+ force.include?(key)
131
+ end
132
+
133
+ def nested?
134
+ !!restful_options[:nested]
135
+ end
136
+
137
+ def nested(key)
138
+ definition = @whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first
139
+ Config.new((definition[key] if definition))
140
+ end
141
+
142
+ private
143
+
144
+ def split_into_whitelist_and_restful_options(array)
145
+ options = {}
146
+
147
+ return array.map do |el|
148
+ if el.is_a? Hash
149
+ el = el.clone
150
+ deleted = el.delete(:restful_options)
151
+ options.merge!(deleted) if deleted
152
+ el = nil if el == {}
153
+ end
154
+
155
+ el
156
+ end.compact, options
157
+ end
158
+
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ ActiveRecord::Base.send :include, Restful::Rails::ActiveRecord::Configuration
166
+ Array.send :include, Restful::Rails::ActiveRecord::Configuration::CommonInstanceMethods
167
+ Hash.send :include, Restful::Rails::ActiveRecord::Configuration::CommonInstanceMethods
@@ -0,0 +1,106 @@
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.link(key, model, config)
53
+ Restful.link(key.to_sym, "base", "path", "link")
54
+ end
55
+
56
+ def self.dereference(url)
57
+ regexp = Regexp.new("#{ Restful::Rails.api_hostname }\/(.*)\/(.*)")
58
+ m, resource, params = *url.match(regexp)
59
+ resource = if resource && params
60
+ clazz = resource.try(:singularize).try(:camelize).try(:constantize)
61
+ clazz.find_by_restful(params) if clazz
62
+ end
63
+
64
+ resource ? resource.id : 0
65
+ end
66
+
67
+ # retruns non association / collection attributes.
68
+ def self.simple_attributes_on(model)
69
+ attributes = model.attributes
70
+
71
+ attributes.delete_if do |k, v|
72
+ model.class.apiable_association_table && model.class.apiable_association_table.keys.include?(k)
73
+ end
74
+ end
75
+
76
+ #
77
+ # Takes an ar model and a key like :people, and returns an array of resources.
78
+ #
79
+ # TODO: don't load the entire association, only the published attributes (with an appropriate :select).
80
+ # TODO: get some pagination in.
81
+ #
82
+ def self.convert_collection_to_resources(model, key, config)
83
+
84
+ # load the associated objects.
85
+ models = model.send(key)
86
+
87
+ # convert them to_restful.
88
+ if models
89
+ [*models].map do |m|
90
+ if m.respond_to? :to_restful
91
+ config.nested? ?
92
+ link(key, m, config) :
93
+ expand(m, config)
94
+ else
95
+ raise "Seems as if you want to export the relation #{ key } of an #{ model.class.to_s } object without making #{ key } apiable."
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ 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,57 @@
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 = {}) # implement me.
10
+ raise NotImplementedError.new
11
+ end
12
+
13
+ def deserialize(resource, options = {}) # implement me.
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 formatted_value(value)
32
+ return value unless value.respond_to?(:value)
33
+
34
+ return nil if value.value.blank? && value.extended_type != :false_class
35
+ case value.extended_type
36
+ when :datetime
37
+ value.value.xmlschema
38
+ when :time
39
+ value.value.xmlschema
40
+ when :date
41
+ value.value.to_s(:db)
42
+ else
43
+ value.value
44
+ end
45
+ end
46
+
47
+ protected
48
+ def transform_link_name(name)
49
+ name.to_s.gsub /_id$/, "-restful-url"
50
+ end
51
+
52
+ def revert_link_name(name)
53
+ name.to_s.gsub /-restful-url$/, "_id"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ require 'restful/serializers/base'
2
+
3
+ #
4
+ # AR params hash.
5
+ #
6
+ module Restful
7
+ module Serializers
8
+ class HashSerializer < Base
9
+
10
+ serializer_name :hash
11
+
12
+ def serialize(resource, options = {})
13
+ case resource
14
+ when Restful::ApiModel::Collection then serialize_collection(resource)
15
+ when Restful::ApiModel::Resource then serialize_tuples(resource.values, resource.full_url)
16
+ when Restful::ApiModel::Map then serialize_map(resource)
17
+ else
18
+ serialize_tuples(resource.values, resource.full_url)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_collection(collection)
25
+ values = collection.value.map { |r| serialize(r) }
26
+
27
+ if entries = collection.total_entries
28
+ values = { :total_entries => entries, collection.name => values }
29
+ end
30
+
31
+ values
32
+ end
33
+
34
+ def serialize_map(map)
35
+ map.values.inject({}) do |memo, attribute|
36
+ memo[attribute.name] = serialize_value(attribute.value)
37
+ memo
38
+ end
39
+ end
40
+
41
+ def serialize_tuples(tuples, url)
42
+ tuples.inject({ "restful_url" => url }) do |params, value|
43
+ params[value.name.to_s.tr("-", "_").to_sym] = serialize_value(value)
44
+ params
45
+ end
46
+ end
47
+
48
+ def serialize_value(value)
49
+ case value.type
50
+ when :collection then serialize_collection(value)
51
+ when :link then Restful::Rails.tools.dereference(value.value)
52
+ when :resource then serialize(value)
53
+ else formatted_value(value)
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end