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,20 @@
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
+ hasher = Restful::Serializers::HashSerializer.new
15
+ hash = hasher.serialize(resource, options)
16
+ Yajl::Encoder.encode(hash)
17
+ end
18
+ end
19
+ end
20
+ 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,161 @@
1
+ require 'restful/serializers/base'
2
+ require "rexml/document"
3
+ require 'builder'
4
+ require 'ruby-debug'
5
+
6
+ #
7
+ # Converts an APIModel to and from XML.
8
+ #
9
+ module Restful
10
+ module Serializers
11
+ class XMLSerializer < Base
12
+
13
+ serializer_name :xml
14
+
15
+ def serialize(resource, options = {})
16
+
17
+ xml = options[:builder] || Builder::XmlMarkup.new(:indent => 2)
18
+ xml.instruct! unless options[:instruct].is_a?(FalseClass)
19
+
20
+ raise NotImplementedError.new("xml serialization of maps has not been implemented. ") if resource.class == Restful::ApiModel::Map
21
+
22
+ if resource.is_a?(Restful::ApiModel::Collection)
23
+ add_collection(resource, xml, show_as_array = false)
24
+ else
25
+ xml.tag!(*root_element(resource)) do
26
+ add_link_to(resource, xml, :self => true)
27
+
28
+ resource.values.each do |value|
29
+ if value.type == :collection # serialize the stuffs
30
+ add_collection(value, xml)
31
+ elsif value.type == :link
32
+ add_link_to(value, xml)
33
+ elsif value.type == :resource
34
+ serialize(value, {:instruct => false, :builder => xml})
35
+ else # plain ole
36
+ add_tag(xml, value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # returns a resource, or collection of resources.
44
+ def deserialize(xml, options = {})
45
+ build_resource(REXML::Document.new(xml).root)
46
+ end
47
+
48
+ protected
49
+
50
+ def add_collection(value, xml, show_as_array = true)
51
+ resources = value.value
52
+ if first_resource = resources.first
53
+ xml.tag!(first_resource.name.pluralize, (show_as_array ? collections_decorations : {})) do
54
+ resources.each do |resource|
55
+ serialize(resource, { :instruct => false, :builder => xml })
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def add_link_to(resource, builder, options = {})
62
+ is_self = !!options[:self]
63
+
64
+ attributes = {:type => "link"}
65
+ attributes.merge!(:nil => "true") if resource.full_url.blank?
66
+ builder.tag!((is_self ? "restful-url" : transform_link_name(resource.name)), resource.full_url, attributes)
67
+ end
68
+
69
+ def add_tag(builder, value)
70
+
71
+ if value.extended_type == :hash
72
+ build_hash(builder, value)
73
+ else
74
+ builder.tag!(value.name.to_s.dasherize, formatted_value(value), decorations(value))
75
+ end
76
+ end
77
+
78
+ def build_hash(builder, value)
79
+ builder.tag!(value.name.to_s.dasherize) do
80
+ value.value.each do |k, v|
81
+ builder.tag! k.to_s.dasherize, v
82
+ end
83
+ end
84
+ end
85
+
86
+ def decorations(value)
87
+ decorations = {}
88
+
89
+ if value.extended_type == :binary
90
+ decorations[:encoding] = 'base64'
91
+ end
92
+
93
+ if value.extended_type != :string and value.extended_type != :notype
94
+ decorations[:type] = value.extended_type
95
+ end
96
+
97
+ if value.extended_type == :datetime
98
+ decorations[:type] = :datetime
99
+ end
100
+
101
+ if value.value.nil?
102
+ decorations[:nil] = true
103
+ end
104
+
105
+ if value.value.is_a?(FalseClass) || value.value.is_a?(TrueClass)
106
+ decorations[:type] = :boolean
107
+ end
108
+
109
+ decorations
110
+ end
111
+
112
+ def collections_decorations
113
+ { :type => "array" }
114
+ end
115
+
116
+ def root_element(resource, options = {})
117
+ [resource.name]
118
+ end
119
+
120
+ # turns a rexml node into a Resource
121
+ def build_resource(node)
122
+ resource = root_resource(node)
123
+
124
+ node.elements.each do |el|
125
+ type = calculate_node_type(el)
126
+ resource.values << case type
127
+
128
+ when :link : build_link(el, type)
129
+ when :datetime
130
+ Restful.attr(el.name, DateTime.parse(el.text), type)
131
+ when :resource
132
+ build_resource(el)
133
+ when :array
134
+ Restful.collection(el.name, el.elements.map { |child| build_resource(child) }, type)
135
+ else
136
+ Restful.attr(el.name, el.text, type)
137
+ end
138
+ end
139
+
140
+ resource
141
+ end
142
+
143
+ def calculate_node_type(el)
144
+ if el.children.size > 1 && el.attributes["type"].blank?
145
+ return :resource
146
+ else
147
+ (el.attributes["type"] || "string").to_sym
148
+ end
149
+ end
150
+
151
+ def build_link(el, type)
152
+ Restful.link(revert_link_name(el.name), nil, el.text, type)
153
+ end
154
+
155
+ def root_resource(node)
156
+ url = node.delete_element("restful-url").try(:text)
157
+ Restful.resource(node.name, :url => url)
158
+ end
159
+ end
160
+ end
161
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'restful'
data/restful.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "restful"
3
+ s.version = "0.2.8"
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,122 @@
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 be able to force expansion. force expanded attributes can never be collapsed. " do
21
+ Wallet.restful_publish(:contents)
22
+ Person.restful_publish(:name, :wallet, :current_location, { :pets => [:name, :species], :restful_options => { :force_expand => :wallet } })
23
+ Pet.restful_publish(:owner, :name)
24
+
25
+ @pet.to_restful
26
+ end
27
+
28
+ specify "should return link attributes from a model" do
29
+ @pet.to_restful.links.map { |node| node.name }.sort.should.equal [:person_id]
30
+ end
31
+
32
+ specify "should convert a NULL inner association such as person.wallet to a link with a null value" do
33
+ @person.wallet = nil
34
+
35
+ wallet = @person.to_restful(:restful_options => { :expansion => :collapsed }).links.select { |link| link.name == "wallet-restful-url" }.first
36
+ wallet.should.not.== nil
37
+ wallet.value.should.== nil
38
+ end
39
+
40
+ specify "should return plain attributes from a model" do
41
+ @pet.to_restful.simple_attributes.map { |node| node.name }.should.equal [:name]
42
+ end
43
+
44
+ specify "should return collections attributes from a model" do
45
+ restful = @person.to_restful
46
+ restful.collections.map { |node| node.name }.sort.should.equal [:pets]
47
+ end
48
+
49
+ specify "should set correct type for date" do
50
+ restful = @person.to_restful :birthday
51
+ restful.simple_attributes.detect { |node| node.name == :birthday }.extended_type.should.== :date
52
+ end
53
+
54
+ specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api" do
55
+ resource = @person.to_restful
56
+
57
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
58
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
59
+
60
+ mietze = @person.to_restful.collections .select { |node| node.name == :pets }.first.value.first
61
+ mietze.simple_attributes.size.should.== 2
62
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
63
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
64
+ end
65
+
66
+ 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
67
+ resource = @person.to_restful(:restful_options => { :nested => false })
68
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
69
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
70
+
71
+ mietze = resource.collections .select { |node| node.name == :pets }.first.value.first
72
+ mietze.simple_attributes.size.should.== 2
73
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
74
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
75
+ end
76
+
77
+ specify "should be able to override to_restful published fields by passing them into the method" do
78
+ api = @person.to_restful(:pets)
79
+
80
+ api.simple_attributes.should.blank?
81
+ api.collections.map { |node| node.name }.sort.should.equal [:pets]
82
+ end
83
+
84
+ specify "should be able to handle relations that are nil/null" do
85
+ @person.wallet = nil
86
+ @person.save!
87
+ @person.reload
88
+
89
+ assert_nothing_raised do
90
+ @person.to_restful
91
+ end
92
+ end
93
+
94
+ specify "should be able to expand a :belongs_to relationship" do
95
+ xml_should_eql_fixture(@pet.to_restful_xml(:owner), "pets", :nameless_pet)
96
+ end
97
+
98
+ specify "should return collapsed resources by default when :expansion => :collapsed is passed" do
99
+ Person.restful_publish(:name, :wallet, :restful_options => { :expansion => :collapsed })
100
+ xml_should_eql_fixture(@person.to_restful_xml, "people", :joe_bloggs)
101
+ end
102
+
103
+ specify "should be able to export content generated by methods that return strings" do
104
+ xml_should_eql_fixture(@person.to_restful_xml(:location_sentence), "people", :no_wallet)
105
+ end
106
+
107
+ specify "should be able to export content generated by methods (not attributes) and compute the correct style" do
108
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet), "people", :with_oldest_pet)
109
+ end
110
+
111
+ specify "should be able to export content generated by methods (not attributes) while filtering with a nested configuration" do
112
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet => [:species]), "people", :with_oldest_pet_species)
113
+ end
114
+
115
+ specify "should create element with nil='true' attribute if no relation is set" do
116
+ @person.wallet = nil
117
+ @person.save
118
+
119
+ xml_should_eql_fixture(@person.to_restful_xml(:wallet), "people", :joe_with_zwiebelleder)
120
+ end
121
+
122
+ end
@@ -0,0 +1,48 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ context "basic types converter" do
4
+ teardown { reset_config }
5
+
6
+ specify "should raise exception if not all array contents respond to .to_restful" do
7
+ Person.restful_publish(:name)
8
+
9
+ should.raise(TypeError) do
10
+ [""].to_restful
11
+ end
12
+ end
13
+
14
+ specify "should convert an empty array to a restful collection" do
15
+ collection = [].to_restful
16
+ collection.name.should.== "nil-classes"
17
+ end
18
+
19
+ specify "should convert an array to a restful collection" do
20
+ Person.restful_publish(:name)
21
+
22
+ collection = [Person.create(:name => "Joe Bloggs")].to_restful
23
+ collection.name.should.== "people"
24
+ collection.value.size.should.== 1
25
+ collection.value.first.simple_attributes.first.value.should.== "Joe Bloggs"
26
+ end
27
+
28
+ specify "should set total_entries on the restful collection if the array responds to this" do
29
+ Person.restful_publish(:name)
30
+ people = PaginatedCollection.new([Person.create(:name => "Joe Bloggs")])
31
+ people.total_entries = 1001
32
+
33
+ collection = people.to_restful
34
+ collection.total_entries.should.== 1001
35
+ end
36
+
37
+ specify "should be able to convert a hash to a resource map" do
38
+ Person.restful_publish(:name)
39
+ resource = { "zeperson" => @person = Person.create(:name => "fuddzle") }.to_restful
40
+ resource.should.is_a?(Restful::ApiModel::Map)
41
+
42
+ attrs = resource.simple_attributes
43
+ attrs.size.should.== 1
44
+ attrs.first.name.should.== "zeperson"
45
+
46
+ attrs.first.value.values.first.value.== "fuddzle"
47
+ end
48
+ end
@@ -0,0 +1,4 @@
1
+ class PaginatedCollection < Array
2
+ attr_accessor :total_entries
3
+ end
4
+
@@ -0,0 +1,29 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :pets
3
+ has_one :wallet
4
+
5
+ accepts_nested_attributes_for :pets
6
+ accepts_nested_attributes_for :wallet
7
+
8
+ def oldest_pet
9
+ pets.first :order => "age DESC"
10
+ end
11
+
12
+ def location_sentence
13
+ "Hi. I'm currently in #{ current_location }"
14
+ end
15
+
16
+ def has_pets
17
+ pets.size > 0
18
+ end
19
+
20
+ def pets_ages_hash
21
+ returning pets_hash = {} do
22
+ pets.each do |pet|
23
+ pets_hash[pet.name] = pet.age
24
+ end
25
+ end
26
+ end
27
+
28
+ apiable
29
+ end