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