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,46 @@
1
+ require 'restful/serializers/base'
2
+
3
+ #
4
+ # Vanilla Hash.
5
+ #
6
+ module Restful
7
+ module Serializers
8
+ class HashSerializer < Base
9
+
10
+ serializer_name :hash
11
+
12
+ def serialize(obj, options = {})
13
+ case obj.type
14
+ when :link then obj.value
15
+ when :simple_attribute then serialize(obj.value)
16
+ when :collection then serialize_collection(obj)
17
+ when :map then serialize_array_of_apimodels(obj.values)
18
+ when :resource then serialize_array_of_apimodels(obj.values, { "restful_url" => obj.full_url })
19
+ else
20
+ formatted_ruby_type(obj)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def serialize_collection(collection)
27
+ if entries = collection.total_entries
28
+ { :total_entries => entries, collection.name => serialize_unpaginated_collection(collection) }
29
+ else
30
+ serialize_unpaginated_collection(collection)
31
+ end
32
+ end
33
+
34
+ def serialize_unpaginated_collection(collection)
35
+ collection.value.map { |r| serialize(r) }
36
+ end
37
+
38
+ def serialize_array_of_apimodels(apimodels, defaults = {})
39
+ apimodels.inject(defaults) do |memo, apimodel|
40
+ memo[apimodel.name.to_s.underscore.to_sym] = serialize(apimodel)
41
+ memo
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ require 'restful/serializers/base'
2
+ require 'yajl'
3
+
4
+ #
5
+ # AR params hash.
6
+ #
7
+ module Restful
8
+ module Serializers
9
+ class JsonSerializer < Base
10
+
11
+ serializer_name :json
12
+
13
+ def serialize(resource, options = {})
14
+ Yajl::Encoder.encode HashSerializer.new.serialize(resource, options)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ require 'restful/serializers/base'
2
+ require 'builder'
3
+
4
+ #
5
+ # AR params hash.
6
+ #
7
+ module Restful
8
+ module Serializers
9
+ class ParamsSerializer < Base
10
+
11
+ serializer_name :params
12
+
13
+ def serialize(resource, options = {})
14
+ params = {}
15
+ resource.values.each do |value|
16
+ if value.type == :collection # serialize the stuffs
17
+ resources = value.value
18
+ next if resources.empty?
19
+ name = resources.first.name.pluralize
20
+
21
+ array = []
22
+ resources.each do |resource|
23
+ array << serialize(resource)
24
+ end
25
+
26
+ params["#{paramify_keys(value.name)}_attributes".to_sym] = array
27
+ elsif value.type == :link
28
+ params[paramify_keys(value.name).to_sym] = Restful::Rails.tools.dereference(value.value)
29
+ elsif value.type == :resource
30
+ params["#{paramify_keys(value.name)}_attributes".to_sym] = serialize(value)
31
+ else # plain ole
32
+ params[paramify_keys(value.name).to_sym] = value.value # no need to format dates etc - just pass objects through.
33
+ end
34
+ end
35
+
36
+ params
37
+ end
38
+
39
+ private
40
+
41
+ def paramify_keys(original_key)
42
+ original_key.to_s.tr("-", "_")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,160 @@
1
+ require 'restful/serializers/base'
2
+ require "rexml/document"
3
+ require 'builder'
4
+
5
+ #
6
+ # Converts an APIModel to and from XML.
7
+ #
8
+ module Restful
9
+ module Serializers
10
+ class XMLSerializer < Base
11
+
12
+ serializer_name :xml
13
+
14
+ def serialize(resource, options = {})
15
+
16
+ xml = options[:builder] || Builder::XmlMarkup.new(:indent => 2)
17
+ xml.instruct! unless options[:instruct].is_a?(FalseClass)
18
+
19
+ raise NotImplementedError.new("xml serialization of maps has not been implemented. ") if resource.class == Restful::ApiModel::Map
20
+
21
+ if resource.is_a?(Restful::ApiModel::Collection)
22
+ add_collection(resource, xml, show_as_array = false)
23
+ else
24
+ xml.tag!(*root_element(resource)) do
25
+ add_link_to(resource, xml, :self => true)
26
+
27
+ resource.values.each do |value|
28
+ if value.type == :collection # serialize the stuffs
29
+ add_collection(value, xml)
30
+ elsif value.type == :link
31
+ add_link_to(value, xml)
32
+ elsif value.type == :resource
33
+ serialize(value, {:instruct => false, :builder => xml})
34
+ else # plain ole
35
+ add_tag(xml, value)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # returns a resource, or collection of resources.
43
+ def deserialize(xml, options = {})
44
+ build_resource(REXML::Document.new(xml).root)
45
+ end
46
+
47
+ protected
48
+
49
+ def add_collection(value, xml, show_as_array = true)
50
+ resources = value.value
51
+ if first_resource = resources.first
52
+ xml.tag!(first_resource.name.pluralize, (show_as_array ? collections_decorations : {})) do
53
+ resources.each do |resource|
54
+ serialize(resource, { :instruct => false, :builder => xml })
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def add_link_to(resource, builder, options = {})
61
+ is_self = !!options[:self]
62
+
63
+ attributes = {:type => "link"}
64
+ attributes.merge!(:nil => "true") if resource.full_url.blank?
65
+ builder.tag!((is_self ? "restful-url" : transform_link_name(resource.name)), resource.full_url, attributes)
66
+ end
67
+
68
+ def add_tag(builder, value)
69
+
70
+ if value.extended_type == :hash
71
+ build_hash(builder, value)
72
+ else
73
+ builder.tag!(value.name.to_s.dasherize, serialize_attribute(value), decorations(value))
74
+ end
75
+ end
76
+
77
+ def build_hash(builder, value)
78
+ builder.tag!(value.name.to_s.dasherize) do
79
+ value.value.each do |k, v|
80
+ builder.tag! k.to_s.dasherize, v
81
+ end
82
+ end
83
+ end
84
+
85
+ def decorations(value)
86
+ decorations = {}
87
+
88
+ if value.extended_type == :binary
89
+ decorations[:encoding] = 'base64'
90
+ end
91
+
92
+ if value.extended_type != :string and value.extended_type != :notype
93
+ decorations[:type] = value.extended_type
94
+ end
95
+
96
+ if value.extended_type == :datetime
97
+ decorations[:type] = :datetime
98
+ end
99
+
100
+ if value.value.nil?
101
+ decorations[:nil] = true
102
+ end
103
+
104
+ if value.value.is_a?(FalseClass) || value.value.is_a?(TrueClass)
105
+ decorations[:type] = :boolean
106
+ end
107
+
108
+ decorations
109
+ end
110
+
111
+ def collections_decorations
112
+ { :type => "array" }
113
+ end
114
+
115
+ def root_element(resource, options = {})
116
+ [resource.name]
117
+ end
118
+
119
+ # turns a rexml node into a Resource
120
+ def build_resource(node)
121
+ resource = root_resource(node)
122
+
123
+ node.elements.each do |el|
124
+ type = calculate_node_type(el)
125
+ resource.values << case type
126
+
127
+ when :link : build_link(el, type)
128
+ when :datetime
129
+ Restful.attr(el.name, DateTime.parse(el.text), type)
130
+ when :resource
131
+ build_resource(el)
132
+ when :array
133
+ Restful.collection(el.name, el.elements.map { |child| build_resource(child) }, type)
134
+ else
135
+ Restful.attr(el.name, el.text, type)
136
+ end
137
+ end
138
+
139
+ resource
140
+ end
141
+
142
+ def calculate_node_type(el)
143
+ if el.children.size > 1 && el.attributes["type"].blank?
144
+ return :resource
145
+ else
146
+ (el.attributes["type"] || "string").to_sym
147
+ end
148
+ end
149
+
150
+ def build_link(el, type)
151
+ Restful.link(revert_link_name(el.name), nil, el.text, type)
152
+ end
153
+
154
+ def root_resource(node)
155
+ url = node.delete_element("restful-url").try(:text)
156
+ Restful.resource(node.name, :url => url)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1 @@
1
+ require 'restful'
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "restful"
3
+ s.version = "0.2.20"
4
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
5
+ s.authors = ["Daniel Bornkessel", "Rany Keddo"]
6
+ s.date = "2009-08-11"
7
+ s.email = "M4SSIVE@m4ssive.com"
8
+ s.extra_rdoc_files = %w{ README.markdown }
9
+ s.files = %w{ CHANGES.markdown init.rb lib/restful/apimodel/attribute.rb lib/restful/apimodel/collection.rb lib/restful/apimodel/link.rb lib/restful/apimodel/map.rb lib/restful/apimodel/resource.rb lib/restful/converters/active_record.rb lib/restful/rails/action_controller.rb lib/restful/rails/active_record/configuration.rb lib/restful/rails/active_record/metadata_tools.rb lib/restful/rails.rb lib/restful/serializers/atom_like_serializer.rb lib/restful/serializers/base.rb lib/restful/serializers/hash_serializer.rb lib/restful/serializers/json_serializer.rb lib/restful/serializers/params_serializer.rb lib/restful/serializers/xml_serializer.rb lib/restful.rb LICENSE.markdown rails/init.rb Rakefile README.markdown restful.gemspec test/converters/active_record_converter_test.rb test/converters/basic_types_converter_test.rb test/fixtures/models/paginated_collection.rb test/fixtures/models/person.rb test/fixtures/models/pet.rb test/fixtures/models/wallet.rb test/fixtures/people.json.yaml test/fixtures/people.xml.yaml test/fixtures/pets.json.yaml test/fixtures/pets.xml.yaml test/rails/active_record_metadata_test.rb test/rails/configuration_test.rb test/rails/restful_publish_test.rb test/serializers/atom_serializer_test.rb test/serializers/json_serializer_test.rb test/serializers/params_serializer_test.rb test/serializers/xml_serializer_test.rb test/test_helper.rb TODO.markdown }
10
+ s.has_rdoc = true
11
+ s.rdoc_options = ["--line-numbers", "--inline-source"]
12
+ s.homepage = "http://github.com/M4SSIVE/restful"
13
+ s.require_paths = %w{ lib }
14
+ s.requirements = %w{ brianmario-yajl-ruby }
15
+ s.rubygems_version = "1.3.1"
16
+ s.summary = "api niceness. "
17
+ end
@@ -0,0 +1,147 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ #
4
+ # FIXME: remove xml serialzation here and test resource directly.
5
+ #
6
+ context "active record converter" do
7
+ setup do
8
+ Person.restful_publish(:name, :wallet, :current_location, :pets => [:name, :species])
9
+ Pet.restful_publish(:person_id, :name) # person_id gets converted to a link automagically.
10
+
11
+ @person = Person.create(:name => "Joe Bloggs", :current_location => "Under a tree", :birthday => "1976-04-03")
12
+ @wallet = @person.wallet = Wallet.create!(:contents => "something in the wallet")
13
+ @pet = @person.pets.create(:name => "Mietze", :species => "cat")
14
+ end
15
+
16
+ teardown do
17
+ reset_config
18
+ end
19
+
20
+ specify "should publish link and not resource when :oldest_pet_restful_url, where oldest_pet is a defined method" do
21
+ Person.restful_publish(:oldest_pet_restful_url)
22
+ @person.to_restful.links.size.should.== 1
23
+ @person.to_restful.links.first.name.should.== "oldest-pet-restful-url"
24
+ end
25
+
26
+ specify "should publish link and not a nested resource with :wallet_restful_url" do
27
+ Person.restful_publish(:wallet_restful_url)
28
+ @person.to_restful.links.size.should.== 1
29
+ @person.to_restful.links.first.name.should.== "wallet-restful-url"
30
+ @person.to_restful.links.first.value.should.== @wallet.restful_url
31
+ end
32
+
33
+ specify "should be able to force expansion. force expanded attributes can never be collapsed. " do
34
+ Wallet.restful_publish(:contents)
35
+ Person.restful_publish(:name, :wallet, :current_location, { :pets => [:name, :species], :restful_options => { :force_expand => :wallet } })
36
+ Pet.restful_publish(:owner, :name)
37
+
38
+ @pet.to_restful
39
+ end
40
+
41
+ specify "should return link attributes from a model" do
42
+ @pet.to_restful.links.map { |node| node.name }.sort.should.equal [:person_id]
43
+ end
44
+
45
+ specify "should convert a NULL inner association such as person.wallet to a link with a null value" do
46
+ @person.wallet = nil
47
+
48
+ wallet = @person.to_restful(:restful_options => { :expansion => :collapsed }).links.select { |link| link.name == "wallet-restful-url" }.first
49
+ wallet.should.not.== nil
50
+ wallet.value.should.== nil
51
+ end
52
+
53
+ specify "should return plain attributes from a model" do
54
+ @pet.to_restful.simple_attributes.map { |node| node.name }.should.equal [:name]
55
+ end
56
+
57
+ specify "should return collections attributes from a model" do
58
+ restful = @person.to_restful
59
+ restful.collections.map { |node| node.name }.sort.should.equal [:pets]
60
+ end
61
+
62
+ specify "should set correct type for date" do
63
+ restful = @person.to_restful :birthday
64
+ restful.simple_attributes.detect { |node| node.name == :birthday }.extended_type.should.== :date
65
+ end
66
+
67
+ specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api" do
68
+ resource = @person.to_restful
69
+
70
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
71
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
72
+
73
+ mietze = @person.to_restful.collections .select { |node| node.name == :pets }.first.value.first
74
+ mietze.simple_attributes.size.should.== 2
75
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
76
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
77
+ end
78
+
79
+ specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api. this holds true if to_restful is called with some configuration options. " do
80
+ resource = @person.to_restful(:restful_options => { :nested => false })
81
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
82
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
83
+
84
+ mietze = resource.collections .select { |node| node.name == :pets }.first.value.first
85
+ mietze.simple_attributes.size.should.== 2
86
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
87
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
88
+ end
89
+
90
+ specify "should be able to override to_restful published fields by passing them into the method" do
91
+ api = @person.to_restful(:pets)
92
+
93
+ api.simple_attributes.should.blank?
94
+ api.collections.map { |node| node.name }.sort.should.equal [:pets]
95
+ end
96
+
97
+ specify "should be able to handle relations that are nil/null" do
98
+ @person.wallet = nil
99
+ @person.save!
100
+ @person.reload
101
+
102
+ assert_nothing_raised do
103
+ @person.to_restful
104
+ end
105
+ end
106
+
107
+ specify "should be able to expand a :belongs_to relationship" do
108
+ xml_should_eql_fixture(@pet.to_restful_xml(:owner), "pets", :nameless_pet)
109
+ end
110
+
111
+ specify "should return collapsed resources by default when :expansion => :collapsed is passed" do
112
+ Person.restful_publish(:name, :wallet, :restful_options => { :expansion => :collapsed })
113
+ xml_should_eql_fixture(@person.to_restful_xml, "people", :joe_bloggs)
114
+ end
115
+
116
+ specify "should be able to export content generated by methods that return strings" do
117
+ xml_should_eql_fixture(@person.to_restful_xml(:location_sentence), "people", :no_wallet)
118
+ end
119
+
120
+ specify "should be able to export content generated by methods (not attributes) and compute the correct style" do
121
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet), "people", :with_oldest_pet)
122
+ end
123
+
124
+ specify "should be able to export content generated by methods (not attributes) while filtering with a nested configuration" do
125
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet => [:species]), "people", :with_oldest_pet_species)
126
+ end
127
+
128
+ specify "should create element with nil='true' attribute if no relation is set" do
129
+ @person.wallet = nil
130
+ @person.save
131
+
132
+ xml_should_eql_fixture(@person.to_restful_xml(:wallet), "people", :joe_with_zwiebelleder)
133
+ end
134
+
135
+ specify "should include attributes when include parameter is passed to to_restful" do
136
+ Person.restful_publish(:name)
137
+ Pet.restful_publish(:name)
138
+
139
+ @person = Person.create
140
+ @pet = @person.pets.create(:name => "Mietze")
141
+
142
+ @pet.to_restful(:include => :owner).values.map(&:name).should.include :owner
143
+ Pet.restful_config.whitelisted.include?(:owner).should.equal false
144
+ end
145
+
146
+ end
147
+
@@ -0,0 +1,99 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ context "basic types converter" do
4
+
5
+ teardown { reset_config }
6
+
7
+ specify "should be able to convert a hash to a resource map" do
8
+ Person.restful_publish(:name)
9
+ resource = { "zeperson" => @person = Person.create(:name => "fuddzle") }.to_restful
10
+ resource.should.is_a?(Restful::ApiModel::Map)
11
+
12
+ attrs = resource.simple_attributes
13
+ attrs.size.should.== 1
14
+ attrs.first.name.should.== "zeperson"
15
+
16
+ attrs.first.value.values.first.value.== "fuddzle"
17
+ end
18
+
19
+ specify "should raise exception if not all array contents respond to .to_restful" do
20
+ Person.restful_publish(:name)
21
+
22
+ should.raise(TypeError) do
23
+ [""].to_restful
24
+ end
25
+ end
26
+
27
+ specify "should convert an empty array to a restful collection" do
28
+ collection = [].to_restful
29
+ collection.name.should.== "nil-classes"
30
+ end
31
+
32
+ specify "should infer array name from first element class" do
33
+ collection = [Pet.new].to_restful
34
+ collection.name.should.== "pets"
35
+ end
36
+
37
+ specify "should infer array name from first element base_class if it is an active_record object" do
38
+ collection = [Emu.new, Pet.new].to_restful
39
+ collection.name.should.== "pets"
40
+ end
41
+
42
+ specify "should convert an array to a restful collection" do
43
+ Person.restful_publish(:name)
44
+
45
+ collection = [Person.create(:name => "Joe Bloggs")].to_restful
46
+ collection.name.should.== "people"
47
+ collection.value.size.should.== 1
48
+ collection.value.first.simple_attributes.first.value.should.== "Joe Bloggs"
49
+ end
50
+
51
+ specify "should set total_entries on the restful collection if the array responds to this" do
52
+ Person.restful_publish(:name)
53
+ people = PaginatedCollection.new([Person.create(:name => "Joe Bloggs")])
54
+ people.total_entries = 1001
55
+
56
+ collection = people.to_restful
57
+ collection.total_entries.should.== 1001
58
+ end
59
+
60
+ specify "should set name on collection if array responds to .name and has this set" do
61
+ Person.restful_publish(:name)
62
+ people = PaginatedCollection.new()
63
+ people.total_entries = 0
64
+ people.name = "people"
65
+
66
+ collection = people.to_restful
67
+ collection.name.should.== "people"
68
+ end
69
+ end
70
+
71
+ context "basic types converter :includes" do
72
+
73
+ specify "should include extra attributes for hashes" do
74
+ Person.restful_publish(:name)
75
+ Pet.restful_publish(:name)
76
+
77
+ @person = Person.create
78
+ @pet = @person.pets.create(:name => "Mietze")
79
+
80
+ map = { :pet => @pet }.to_restful(:include => :owner)
81
+ map.values.first.name.should.== :pet
82
+ map.values.first.value.values.map(&:name).should.include :owner
83
+
84
+ Pet.restful_config.whitelisted.include?(:owner).should.equal false
85
+ end
86
+
87
+ specify "should include extra attributes for arrays" do
88
+ Person.restful_publish(:name)
89
+ Pet.restful_publish(:name)
90
+
91
+ @person = Person.create
92
+ @pet = @person.pets.create(:name => "Mietze")
93
+
94
+ collection = [@pet].to_restful(:include => :owner)
95
+ collection.value.first.values.map(&:name).should.include :owner
96
+
97
+ Pet.restful_config.whitelisted.include?(:owner).should.equal false
98
+ end
99
+ end