purzelrakete-restful 0.2.1

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 (39) hide show
  1. data/LICENSE.markdown +22 -0
  2. data/README.markdown +108 -0
  3. data/Rakefile +22 -0
  4. data/TODO.markdown +7 -0
  5. data/init.rb +1 -0
  6. data/lib/restful/apimodel/attribute.rb +17 -0
  7. data/lib/restful/apimodel/collection.rb +14 -0
  8. data/lib/restful/apimodel/link.rb +21 -0
  9. data/lib/restful/apimodel/resource.rb +47 -0
  10. data/lib/restful/converters/active_record.rb +128 -0
  11. data/lib/restful/rails/action_controller.rb +14 -0
  12. data/lib/restful/rails/active_record/configuration.rb +114 -0
  13. data/lib/restful/rails/active_record/metadata_tools.rb +106 -0
  14. data/lib/restful/rails.rb +22 -0
  15. data/lib/restful/serializers/atom_like_serializer.rb +51 -0
  16. data/lib/restful/serializers/base.rb +41 -0
  17. data/lib/restful/serializers/hash_serializer.rb +45 -0
  18. data/lib/restful/serializers/json_serializer.rb +20 -0
  19. data/lib/restful/serializers/params_serializer.rb +40 -0
  20. data/lib/restful/serializers/xml_serializer.rb +146 -0
  21. data/lib/restful.rb +64 -0
  22. data/rails/init.rb +1 -0
  23. data/restful.gemspec +17 -0
  24. data/test/converters/active_record_converter_test.rb +108 -0
  25. data/test/fixtures/models/person.rb +17 -0
  26. data/test/fixtures/models/pet.rb +5 -0
  27. data/test/fixtures/models/wallet.rb +5 -0
  28. data/test/fixtures/people.json.yaml +16 -0
  29. data/test/fixtures/people.xml.yaml +105 -0
  30. data/test/fixtures/pets.xml.yaml +21 -0
  31. data/test/rails/active_record_metadata_test.rb +23 -0
  32. data/test/rails/configuration_test.rb +45 -0
  33. data/test/rails/restful_publish_test.rb +41 -0
  34. data/test/serializers/atom_serializer_test.rb +33 -0
  35. data/test/serializers/json_serializer_test.rb +21 -0
  36. data/test/serializers/params_serializer_test.rb +44 -0
  37. data/test/serializers/xml_serializer_test.rb +38 -0
  38. data/test/test_helper.rb +129 -0
  39. metadata +93 -0
@@ -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,41 @@
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
+ protected
32
+ def transform_link_name(name)
33
+ name.to_s.gsub /_id$/, "-restful-url"
34
+ end
35
+
36
+ def revert_link_name(name)
37
+ name.to_s.gsub /-restful-url$/, "_id"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
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
+ params = {}
14
+
15
+ resource.values.each do |value|
16
+ if value.type == :collection # serialize the stuffs
17
+ resources = value.value
18
+ name = resources.first.name.pluralize
19
+
20
+ array = []
21
+ resources.each { |r| array << serialize(r) }
22
+
23
+ params["#{value.name}".to_sym] = array
24
+ elsif value.type == :link
25
+ params[value.name] = Restful::Rails.tools.dereference(value.value)
26
+ elsif value.type == :resource
27
+ params["#{value.name}".to_sym] = serialize(value)
28
+ else # plain ole
29
+ string_value = case value.extended_type
30
+ when :datetime
31
+ value.value.xmlschema
32
+ else
33
+ value.value
34
+ end
35
+
36
+ params[value.name] = string_value
37
+ end
38
+ end
39
+
40
+ params["restful_url"] = resource.full_url
41
+ params
42
+ end
43
+ end
44
+ end
45
+ end
@@ -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,40 @@
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
+
16
+ resource.values.each do |value|
17
+ if value.type == :collection # serialize the stuffs
18
+ resources = value.value
19
+ name = resources.first.name.pluralize
20
+
21
+ array = []
22
+ resources.each do |resource|
23
+ array << serialize(resource)
24
+ end
25
+
26
+ params["#{value.name.to_sym}_attributes".to_sym] = array
27
+ elsif value.type == :link
28
+ params[value.name] = Restful::Rails.tools.dereference(value.value)
29
+ elsif value.type == :resource
30
+ params["#{value.name.to_sym}_attributes".to_sym] = serialize(value)
31
+ else # plain ole
32
+ params[value.name] = value.value
33
+ end
34
+ end
35
+
36
+ params
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,146 @@
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
+ xml = options[:builder] || Builder::XmlMarkup.new(:indent => 2)
17
+ xml.instruct! unless options[:instruct].is_a?(FalseClass)
18
+
19
+ xml.tag!(*root_element(resource)) do
20
+ add_link_to(resource, xml, :self => true)
21
+
22
+ resource.values.each do |value|
23
+
24
+ if value.type == :collection # serialize the stuffs
25
+ resources = value.value
26
+ if first_resource = resources.first
27
+ xml.tag!(first_resource.name.pluralize, collections_decorations) do
28
+ resources.each do |resource|
29
+ serialize(resource, { :instruct => false, :builder => xml })
30
+ end
31
+ end
32
+ end
33
+
34
+ elsif value.type == :link
35
+ add_link_to(value, xml)
36
+ elsif value.type == :resource
37
+ serialize(value, {:instruct => false, :builder => xml})
38
+ else # plain ole
39
+ add_tag(xml, value)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # returns a resource, or collection of resources.
46
+ def deserialize(xml, options = {})
47
+ build_resource(REXML::Document.new(xml).root)
48
+ end
49
+
50
+ protected
51
+
52
+ def add_link_to(resource, builder, options = {})
53
+ is_self = !!options[:self]
54
+
55
+ attributes = {:type => "link"}
56
+ attributes.merge!(:nil => "true") if resource.full_url.blank?
57
+ builder.tag!((is_self ? "restful-url" : transform_link_name(resource.name)), resource.full_url, attributes)
58
+ end
59
+
60
+ def add_tag(builder, value)
61
+ string_value = case value.extended_type
62
+ when :datetime
63
+ value.value.xmlschema
64
+ else
65
+ value.value.to_s
66
+ end
67
+
68
+ builder.tag!(
69
+ value.name.to_s.dasherize,
70
+ string_value,
71
+ decorations(value)
72
+ )
73
+ end
74
+
75
+ def decorations(value)
76
+ decorations = {}
77
+
78
+ if value.extended_type == :binary
79
+ decorations[:encoding] = 'base64'
80
+ end
81
+
82
+ if value.extended_type != :string and value.extended_type != :notype
83
+ decorations[:type] = value.extended_type
84
+ end
85
+
86
+ if value.extended_type == :datetime
87
+ decorations[:type] = :datetime
88
+ end
89
+
90
+ if value.value.nil?
91
+ decorations[:nil] = true
92
+ end
93
+
94
+ decorations
95
+ end
96
+
97
+ def collections_decorations
98
+ { :type => "array" }
99
+ end
100
+
101
+ def root_element(resource, options = {})
102
+ [resource.name]
103
+ end
104
+
105
+ # turns a rexml node into a Resource
106
+ def build_resource(node)
107
+ resource = root_resource(node)
108
+
109
+ node.elements.each do |el|
110
+ type = calculate_node_type(el)
111
+ resource.values << case type
112
+
113
+ when :link : build_link(el, type)
114
+ when :datetime
115
+ Restful.attr(el.name, DateTime.parse(el.text), type)
116
+ when :resource
117
+ build_resource(el)
118
+ when :array
119
+ Restful.collection(el.name, el.elements.map { |child| build_resource(child) }, type)
120
+ else
121
+ Restful.attr(el.name, el.text, type)
122
+ end
123
+ end
124
+
125
+ resource
126
+ end
127
+
128
+ def calculate_node_type(el)
129
+ if el.children.size > 1 && el.attributes["type"].blank?
130
+ return :resource
131
+ else
132
+ (el.attributes["type"] || "string").to_sym
133
+ end
134
+ end
135
+
136
+ def build_link(el, type)
137
+ Restful.link(revert_link_name(el.name), nil, el.text, type)
138
+ end
139
+
140
+ def root_resource(node)
141
+ url = node.delete_element("restful-url").try(:text)
142
+ Restful.resource(node.name, :url => url)
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/restful.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'restful/rails'
2
+ require 'restful/rails/active_record/configuration'
3
+ require 'restful/rails/active_record/metadata_tools'
4
+ require 'restful/rails/action_controller'
5
+ require 'restful/converters/active_record'
6
+ require 'restful/apimodel/resource'
7
+ require 'restful/apimodel/attribute'
8
+ require 'restful/apimodel/collection'
9
+ require 'restful/apimodel/link'
10
+ require 'restful/serializers/xml_serializer'
11
+ require 'restful/serializers/json_serializer'
12
+ require 'restful/serializers/atom_like_serializer'
13
+ require 'restful/serializers/params_serializer'
14
+ require 'restful/serializers/hash_serializer'
15
+
16
+ module Restful
17
+
18
+ MAJOR = 0
19
+ MINOR = 1
20
+ REVISION = 2
21
+ VERSION = [MAJOR, MINOR, REVISION].join(".")
22
+
23
+ #
24
+ # Restful.from_xml, #from_atom_like. Methods correspond with
25
+ # resgistered serializers.
26
+ #
27
+ def self.method_missing(method, *args, &block)
28
+ if method.to_s.match(/^from_(.*)$/)
29
+ if serializer_clazz = Restful::Serializers::Base.serializers[type = $1.to_sym]
30
+ s = serializer_clazz.new
31
+ s.deserialize(args.first)
32
+ else
33
+ super
34
+ end
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ #
41
+ # Shortcuts past the namespaces
42
+ #
43
+
44
+ def self.cfg(*options)
45
+ Restful::Rails::ActiveRecord::Configuration::Config.new(*options)
46
+ end
47
+
48
+ def self.attr(*options)
49
+ Restful::ApiModel::Attribute.new(*options)
50
+ end
51
+
52
+ def self.link(*options)
53
+ Restful::ApiModel::Link.new(*options)
54
+ end
55
+
56
+ def self.collection(*options)
57
+ Restful::ApiModel::Collection.new(*options)
58
+ end
59
+
60
+ def self.resource(*options)
61
+ Restful::ApiModel::Resource.new(*options)
62
+ end
63
+
64
+ 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.1"
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{ init.rb lib/restful/apimodel/attribute.rb lib/restful/apimodel/collection.rb lib/restful/apimodel/link.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/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.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,108 @@
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")
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 return link attributes from a model" do
21
+ @pet.to_restful.links.map { |node| node.name }.sort.should.equal [:person_id]
22
+ end
23
+
24
+ specify "should convert a NULL inner association such as person.wallet to a link with a null value" do
25
+ @person.wallet = nil
26
+
27
+ wallet = @person.to_restful(:restful_options => { :expansion => :collapsed }).links.select { |link| link.name == "wallet-restful-url" }.first
28
+ wallet.should.not.== nil
29
+ wallet.value.should.== nil
30
+ end
31
+
32
+ specify "should return plain attributes from a model" do
33
+ @pet.to_restful.simple_attributes.map { |node| node.name }.should.equal [:name]
34
+ end
35
+
36
+ specify "should return collections attributes from a model" do
37
+ restful = @person.to_restful
38
+ restful.collections.map { |node| node.name }.sort.should.equal [:pets]
39
+ end
40
+
41
+ specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api" do
42
+ resource = @person.to_restful
43
+
44
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
45
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
46
+
47
+ mietze = @person.to_restful.collections .select { |node| node.name == :pets }.first.value.first
48
+ mietze.simple_attributes.size.should.== 2
49
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
50
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
51
+ end
52
+
53
+ 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
54
+ resource = @person.to_restful(:restful_options => { :nested => false })
55
+ resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
56
+ resource.simple_attributes.select { |node| node.name == :biography }.should.blank
57
+
58
+ mietze = resource.collections .select { |node| node.name == :pets }.first.value.first
59
+ mietze.simple_attributes.size.should.== 2
60
+ mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
61
+ mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
62
+ end
63
+
64
+ specify "should be able to override to_restful published fields by passing them into the method" do
65
+ api = @person.to_restful(:pets)
66
+
67
+ api.simple_attributes.should.blank?
68
+ api.collections.map { |node| node.name }.sort.should.equal [:pets]
69
+ end
70
+
71
+ specify "should be able to handle relations that are nil/null" do
72
+ @person.wallet = nil
73
+ @person.save!
74
+ @person.reload
75
+
76
+ assert_nothing_raised do
77
+ @person.to_restful
78
+ end
79
+ end
80
+
81
+ specify "should be able to expand a :belongs_to relationship" do
82
+ xml_should_eql_fixture(@pet.to_restful_xml(:owner), "pets", :nameless_pet)
83
+ end
84
+
85
+ specify "should return collapsed resources by default when :expansion => :collapsed is passed" do
86
+ Person.restful_publish(:name, :wallet, :restful_options => { :expansion => :collapsed })
87
+ xml_should_eql_fixture(@person.to_restful_xml, "people", :joe_bloggs)
88
+ end
89
+
90
+ specify "should be able to export content generated by methods that return strings" do
91
+ xml_should_eql_fixture(@person.to_restful_xml(:location_sentence), "people", :no_wallet)
92
+ end
93
+
94
+ specify "should be able to export content generated by methods (not attributes) and compute the correct style" do
95
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet), "people", :with_oldest_pet)
96
+ end
97
+
98
+ specify "should be able to export content generated by methods (not attributes) while filtering with a nested configuration" do
99
+ xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet => [:species]), "people", :with_oldest_pet_species)
100
+ end
101
+
102
+ specify "should create element with nil='true' attribute if no relation is set" do
103
+ @person.wallet = nil
104
+ @person.save
105
+
106
+ xml_should_eql_fixture(@person.to_restful_xml(:wallet), "people", :joe_with_zwiebelleder)
107
+ end
108
+ end
@@ -0,0 +1,17 @@
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
+ apiable
17
+ end
@@ -0,0 +1,5 @@
1
+ class Pet < ActiveRecord::Base
2
+ belongs_to :owner, :class_name => "Person", :foreign_key => "person_id"
3
+
4
+ apiable
5
+ end
@@ -0,0 +1,5 @@
1
+ class Wallet < ActiveRecord::Base
2
+ belongs_to :person
3
+
4
+ apiable
5
+ end
@@ -0,0 +1,16 @@
1
+ bloggs:
2
+ |
3
+ {
4
+ "restful_url": "http://example.com:3000/people/<%= @person.to_param %>",
5
+ "wallet": {
6
+ "restful_url": "http://example.com:3000/wallets/<%= @person.wallet.to_param %>",
7
+ "contents": "an old photo, 5 euros in coins"
8
+ },
9
+ "current_location": "Under a tree",
10
+ "name": "Joe Bloggs",
11
+ "pets": [ {
12
+ "restful_url": "http://example.com:3000/pets/<%= @person.pets.first.to_param %>",
13
+ "name": "mietze"
14
+ }],
15
+ "created_at": "<%= @person.created_at.xmlschema %>"
16
+ }