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,40 @@
1
+ 17. Aug 2009 - 0.2.6
2
+
3
+ * added :include option in to_restful.
4
+
5
+ 18. Aug 2009 - 0.2.7
6
+
7
+ * fixed issue where configurations where overwriting each other.
8
+ * added hash#to_restful
9
+
10
+ 19. Aug 2009 - 0.2.12
11
+
12
+ * added ability to publish :wallet-restful-url (explicitly collapsed)
13
+
14
+ 20. Aug 2009
15
+
16
+ - 0.2.13
17
+
18
+ * hash serializer no longer dereferences ids
19
+
20
+ - 0.2.14
21
+
22
+ * arrays names now use base_class of content models
23
+ * restful_path defaults to using base_class in path
24
+ * if array responds to name, use this as collection name
25
+
26
+ - 0.2.15
27
+
28
+ * :includes like active record to_xml to_json
29
+
30
+ 21. Aug 2009
31
+
32
+ * refactored HashSerializer to be much clearer
33
+
34
+ 26. Aug 2009
35
+
36
+ * fixed issue with link collapsing
37
+
38
+ 28. Aug 2009
39
+
40
+ * fixed cascading of :includes
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2009 SalesKing GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,126 @@
1
+ # Disclaimer
2
+
3
+ !!! Refactor this shice. Seriously, this has devolved into some nasty-ass code.
4
+
5
+ # Why?
6
+
7
+ Aims to provide a production quality Rest API to your Rails app, with the following features:
8
+
9
+ * whitelisting
10
+ * flexible xml formats with good defaults
11
+ * all resources are referred to by url and not by id; expose a "web of resources"
12
+
13
+ # Serializers
14
+
15
+ Getting started
16
+ ============================
17
+ In order to make your models apiable add
18
+
19
+ `apiable`
20
+
21
+ to your model. Next, define which properties you want to export, so within the model write something like:
22
+
23
+ `self.restful_publish(:name, :current-location, :pets)`
24
+
25
+ Configuration
26
+ =============
27
+
28
+ Some example configurations:
29
+
30
+ # Person
31
+ restful_publish :name, :pets, :restful_options => { :expansion => :expanded } # default on level 1-2: expanded. default above: collapsed.
32
+ restful_publish :name, :pets, :wallet => :contents, :restful_options => { :expansion => :expanded } # combined options and expansion rules
33
+ restful_publish :name, :pets, :restful_options => { :collapsed => :pets } # collapsed pets, even though they are on the second level.
34
+ restful_publish :name, :pets, :restful_options => { :force_expanded => [:pets, :wallet] }
35
+
36
+ ## explicitly collapsed
37
+ restful_options :wallet-restful-url
38
+
39
+ # Pet
40
+ restful_publish :name, :person # expands person per default because it is on the second level. Does not expand person.pets.first.person, since this is higher than second level.
41
+
42
+ Options
43
+ =======
44
+
45
+ You can add includes to your call like this:
46
+
47
+ pet.to_restful_json :include => :owner.
48
+
49
+ Rails-like
50
+ ==========
51
+
52
+ This format sticks to xml_simple, adding links as `<association-name-restful-url>` nodes of type "link".
53
+
54
+
55
+ `Person.last.to_restful.serialize(:xml)` OR
56
+ `Person.last.to_restful_xml` results in something like...
57
+
58
+ <?xml version="1.0" encoding="UTF-8"?>
59
+ <person>
60
+ <restful-url type="link">http://example.com:3000/people/1</restful-url>
61
+ <name>Joe Bloggs</name>
62
+ <current-location>Under a tree</current-location>
63
+ <pets type="array">
64
+ <pet>
65
+ <restful-url type="link">http://example.com:3000/pets/1</restful-url>
66
+ <person-restful-url type="link">http://example.com:3000/people/1</person-restful-url>
67
+ <name nil="true"></name>
68
+ </pet>
69
+ </pets>
70
+ <sex>
71
+ <restful-url type="link">http://example.com:3000/sexes/1</restful-url>
72
+ <sex>male</sex>
73
+ </sex>
74
+ </person>
75
+
76
+
77
+ Atom-like
78
+ =========
79
+
80
+ `Person.last.to_restful.serialize(:atom_like)` OR
81
+ `Person.last.to_restful_atom_like` results in something like...
82
+
83
+ <?xml version="1.0" encoding="UTF-8"?>
84
+ <person xml:base="http://example.com:3000">
85
+ <link rel="self" href="/people/1"/>
86
+ <name>Joe Bloggs</name>
87
+ <current-location>Under a tree</current-location>
88
+ <pets>
89
+ <pet>
90
+ <link rel="self" href="/pets/1"/>
91
+ <link rel="person_id" href="/people/1"/>
92
+ <name></name>
93
+ </pet>
94
+ </pets>
95
+ <sex>
96
+ <link rel="self" href="/sexes/1"/>
97
+ <sex>male</sex>
98
+ </sex>
99
+ </person>
100
+
101
+ Params-like
102
+ ===========
103
+
104
+ `Person.last.to_restful.serialize(:params)` OR
105
+ `Person.last.to_restful_params` results in something like...
106
+
107
+ {:sex_attributes => {:sex=>"male"},
108
+ :current_location=>"Under a tree",
109
+ :name=>"Joe Bloggs",
110
+ :pets_attributes=> [ {:person_id=>1, :name=>nil} ]
111
+ }
112
+
113
+ Other Serializers
114
+ =================
115
+
116
+ Hash. Spits out a plain ole hash, no nested attributes or such like. Useful for further conversions.
117
+
118
+ Deserializing
119
+ =============
120
+
121
+ Use `Restful.from_atom_like(xml).serialize(:hash)` to convert from an atom-like formatted xml create to a params hash. Takes care of dereferencing the urls back to ids. Generally, use `Restful.from_<serializer name>(xml)` to get a Resource.
122
+
123
+ Nested Attributes
124
+ =================
125
+ Serializing uses Rails 2.3 notation of nested attributes. For deserializing you will need Rails 2.3 for having nested attributes support and the respective model must have the
126
+ `accepts_nested_attributes_for :<table name>` set accordingly.
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the restful plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the restful plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Restful'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.markdown')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,10 @@
1
+ Refactor this shice. Seriously, this has devolved into some nasty-ass code.
2
+
3
+ * the metamodel is kind of weird. make a better metamodel - how about just using activemodel?
4
+ * remove requirement to call apiable in model classes; replace with restful_publish with no args (or with args.)
5
+ * move configuration object out of rails folder - this is general stuff.
6
+ * remove xml serialization here and test resource directly (in active_record_converter_test)
7
+ * get rid of to_a warning
8
+ * convert underscores to dashes (or not) in serializers instead of converter
9
+ * implement xml serialization of hashes
10
+ * write tests to show that [Person.new].to_restful_xml(:include => :wallet) fails to preserve Person whitelist.
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,65 @@
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/map'
7
+ require 'restful/apimodel/resource'
8
+ require 'restful/apimodel/attribute'
9
+ require 'restful/apimodel/collection'
10
+ require 'restful/apimodel/link'
11
+ require 'restful/serializers/xml_serializer'
12
+ require 'restful/serializers/json_serializer'
13
+ require 'restful/serializers/atom_like_serializer'
14
+ require 'restful/serializers/params_serializer'
15
+ require 'restful/serializers/hash_serializer'
16
+
17
+ module Restful
18
+
19
+ MAJOR = 0
20
+ MINOR = 1
21
+ REVISION = 2
22
+ VERSION = [MAJOR, MINOR, REVISION].join(".")
23
+
24
+ #
25
+ # Restful.from_xml, #from_atom_like. Methods correspond with
26
+ # resgistered serializers.
27
+ #
28
+ def self.method_missing(method, *args, &block)
29
+ if method.to_s.match(/^from_(.*)$/)
30
+ if serializer_clazz = Restful::Serializers::Base.serializers[type = $1.to_sym]
31
+ s = serializer_clazz.new
32
+ s.deserialize(args.first)
33
+ else
34
+ super
35
+ end
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ #
42
+ # Shortcuts past the namespaces
43
+ #
44
+
45
+ def self.cfg(*options)
46
+ Restful::Rails::ActiveRecord::Configuration::Config.new(*options)
47
+ end
48
+
49
+ def self.attr(*options)
50
+ Restful::ApiModel::Attribute.new(*options)
51
+ end
52
+
53
+ def self.link(*options)
54
+ Restful::ApiModel::Link.new(*options)
55
+ end
56
+
57
+ def self.collection(*options)
58
+ Restful::ApiModel::Collection.new(*options)
59
+ end
60
+
61
+ def self.resource(*options)
62
+ Restful::ApiModel::Resource.new(*options)
63
+ end
64
+
65
+ end
@@ -0,0 +1,17 @@
1
+ #
2
+ # Attribute model.
3
+ #
4
+ module Restful
5
+ module ApiModel
6
+ class Attribute
7
+ attr_accessor :name, :value, :type, :extended_type
8
+
9
+ def initialize(name, value, extended_type)
10
+ self.name = name
11
+ self.value = value
12
+ self.extended_type = extended_type
13
+ self.type = :simple_attribute
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ #
2
+ # Collection model. A collection is a named array of Resources.
3
+ #
4
+ module Restful
5
+ module ApiModel
6
+ class Collection < Attribute
7
+ attr_accessor :total_entries
8
+
9
+ def initialize(name, resources, extended_type)
10
+ super
11
+
12
+ self.type = :collection
13
+ end
14
+
15
+ # invoke serialization
16
+ def serialize(type)
17
+ serializer = Restful::Serializers::Base.serializer(type)
18
+ serializer.serialize(self)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ #
2
+ # Link model.
3
+ #
4
+ module Restful
5
+ module ApiModel
6
+ class Link < Attribute
7
+ attr_accessor :base, :path
8
+
9
+ def initialize(name, base, path, extended_type)
10
+ self.base = base
11
+ self.path = path
12
+ super(name, self.full_url, extended_type)
13
+ self.type = :link
14
+ end
15
+
16
+ def full_url
17
+ base.blank? ? path : "#{ base }#{ path }"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Resource model. Something like a DOM model for the api.
3
+ #
4
+ module Restful
5
+ module ApiModel
6
+ class Map
7
+ attr_accessor :values, :name, :type
8
+
9
+ def initialize(name)
10
+ self.name = name
11
+ self.type = :map
12
+ self.values = []
13
+ end
14
+
15
+ def links
16
+ self.values.select { |attribute| attribute.type == :link }
17
+ end
18
+
19
+ def simple_attributes
20
+ self.values.select { |attribute| attribute.type == :simple_attribute }
21
+ end
22
+
23
+ def collections
24
+ self.values.select { |attribute| attribute.type == :collection }
25
+ end
26
+
27
+ # invoke serialization
28
+ def serialize(type)
29
+ serializer = Restful::Serializers::Base.serializer(type)
30
+ serializer.serialize(self)
31
+ end
32
+
33
+ # invoke deserialization
34
+ def deserialize_from(type)
35
+ serializer = Restful::Serializers::Base.serializer(type)
36
+ serializer.deserialize(self)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,23 @@
1
+ #
2
+ # Resource model. Something like a DOM model for the api.
3
+ #
4
+ module Restful
5
+ module ApiModel
6
+ class Resource < Map
7
+ attr_accessor :base, :path, :url
8
+
9
+ def initialize(name, url)
10
+ super(name)
11
+
12
+ self.url = url[:url]
13
+ self.path = url[:path]
14
+ self.base = url[:base]
15
+ self.type = :resource
16
+ end
17
+
18
+ def full_url
19
+ base.blank? ? url : "#{ base }#{ path }"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,160 @@
1
+ #
2
+ # Converts an ActiveRecord model into an ApiModel
3
+ #
4
+ module Restful
5
+ module Converters
6
+ class ActiveRecord
7
+
8
+ def self.convert(model, config, options = {})
9
+ published = []
10
+ nested = config.nested?
11
+
12
+ resource = Restful.resource(
13
+ model.class.to_s.tableize.demodulize.singularize, {
14
+ :base => Restful::Rails.api_hostname,
15
+ :path => model.restful_path,
16
+ :url => model.restful_url
17
+ })
18
+
19
+ explicit_links = config.whitelisted.select { |x| x.class == Symbol && x.to_s.ends_with?("_restful_url") }
20
+ explicit_links.each { |link| config.whitelisted.delete(link) }
21
+ explicit_links.map! { |link| link.to_s.chomp("_restful_url").to_sym }
22
+ config.whitelisted += explicit_links
23
+
24
+ # simple attributes
25
+ resource.values += Restful::Rails.tools.simple_attributes_on(model).map do |key, value|
26
+ convert_to_simple_attribute(key, value, config, published, model)
27
+ end.compact
28
+
29
+ # has_many, has_one, belongs_to
30
+ resource.values += model.class.reflections.keys.map do |key|
31
+ explicit_link = !!explicit_links.include?(key)
32
+
33
+ if config.published?(key.to_sym) || explicit_link
34
+ nested_config = config.nested(key.to_sym)
35
+ published << key.to_sym
36
+
37
+ if has_many?(model, key) && config.expanded?(key, nested)
38
+ convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
39
+ Restful.collection(key, resources, extended_type)
40
+ end
41
+ elsif has_one?(model, key) or belongs_to?(model, key)
42
+ if config.expanded?(key, nested) && !explicit_link
43
+ convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
44
+ returning(resources.first) do |res|
45
+ res.name = key
46
+ end
47
+ end
48
+ else
49
+ link_to(model, key)
50
+ end
51
+ end
52
+ end
53
+ end.compact
54
+
55
+ # Links
56
+ if model.class.apiable_association_table
57
+
58
+ resource.values += model.class.apiable_association_table.keys.map do |key|
59
+
60
+ if config.published?(key.to_sym)
61
+ published << key.to_sym
62
+ base, path = model.resolve_association_restful_url(key)
63
+ Restful.link(key.to_sym, base, path, compute_extended_type(model, key))
64
+ end
65
+ end.compact
66
+ end
67
+
68
+ # public methods
69
+ resource.values += (model.public_methods - Restful::Rails.tools.simple_attributes_on(model).keys.map(&:to_s)).map do |method_name|
70
+
71
+ explicit_link = !!explicit_links.include?(method_name.to_sym)
72
+
73
+ if !published.include?(method_name.to_sym) && (config.published?(method_name.to_sym) || explicit_link)
74
+ value = model.send(method_name.to_sym)
75
+ sanitized_method_name = method_name.tr("!?", "").tr("_", "-").to_sym
76
+
77
+ if value.is_a? ::ActiveRecord::Base
78
+ if config.expanded?(method_name.to_sym, nested) && !explicit_link
79
+ returning Restful::Rails.tools.expand(value, config.nested(method_name.to_sym)) do |expanded|
80
+ expanded.name = sanitized_method_name
81
+ end
82
+ else
83
+ Restful.link("#{ sanitized_method_name }-restful-url", Restful::Rails.api_hostname, value ? value.restful_path : "", compute_extended_type(model, method_name.to_sym))
84
+ end
85
+ else
86
+ Restful.attr(sanitized_method_name, value, compute_extended_type(model, method_name))
87
+ end
88
+ end
89
+ end.compact
90
+
91
+ resource
92
+ end
93
+
94
+ def self.has_one?(model, key)
95
+ macro(model, key) == :has_one
96
+ end
97
+
98
+ def self.has_many?(model, key)
99
+ macro(model, key) == :has_many
100
+ end
101
+
102
+ def self.belongs_to?(model, key)
103
+ macro(model, key) == :belongs_to
104
+ end
105
+
106
+ def self.link_to(model, key)
107
+ value = model.send(key)
108
+ restful_path = value ? value.restful_path : nil
109
+ basename = value ? Restful::Rails.api_hostname : nil
110
+
111
+ Restful.link("#{ key }-restful-url", basename, restful_path, compute_extended_type(model, key))
112
+ end
113
+
114
+ def self.convert_to_simple_attribute(key, value, config, published, model = nil)
115
+ if config.published?(key.to_sym)
116
+ published << key.to_sym
117
+ ext_type = (model ? compute_extended_type(model, key) : value.class.to_s.underscore.to_sym)
118
+ Restful.attr(key.to_sym, value, ext_type)
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def self.macro(model, key)
125
+ model.class.reflections[key].macro
126
+ end
127
+
128
+ def self.convert_to_collection(model, key, nested_config, published)
129
+ if resources = Restful::Rails.tools.convert_collection_to_resources(model, key, nested_config)
130
+ yield key.to_sym, resources, compute_extended_type(model, key)
131
+ else
132
+ published << key.to_sym
133
+ Restful.attr(key.to_sym, nil, :notype)
134
+ end
135
+ end
136
+
137
+ def self.compute_extended_type(record, attribute_name)
138
+ type_symbol = :yaml if record.class.serialized_attributes.has_key?(attribute_name)
139
+
140
+ if column = record.class.columns_hash[attribute_name]
141
+ type_symbol = column.send(:simplified_type, column.sql_type)
142
+ else
143
+
144
+ type_symbol = record.send(attribute_name).class.to_s.underscore.to_sym
145
+ end
146
+
147
+ case type_symbol
148
+ when :text
149
+ :string
150
+ when :time
151
+ :datetime
152
+ when :date
153
+ :date
154
+ else
155
+ type_symbol
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end