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