benjaminkrause-restful 0.2.8

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 +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