benjaminkrause-restful 0.2.8

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 +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
data/CHANGES.markdown ADDED
@@ -0,0 +1,8 @@
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
data/LICENSE.markdown ADDED
@@ -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
+
data/README.markdown ADDED
@@ -0,0 +1,123 @@
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
+ # Pet
37
+ 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.
38
+
39
+ Options
40
+ =======
41
+
42
+ You can add includes to your call like this:
43
+
44
+ pet.to_restful_json :include => :owner.
45
+
46
+ Rails-like
47
+ ==========
48
+
49
+ This format sticks to xml_simple, adding links as `<association-name-restful-url>` nodes of type "link".
50
+
51
+
52
+ `Person.last.to_restful.serialize(:xml)` OR
53
+ `Person.last.to_restful_xml` results in something like...
54
+
55
+ <?xml version="1.0" encoding="UTF-8"?>
56
+ <person>
57
+ <restful-url type="link">http://example.com:3000/people/1</restful-url>
58
+ <name>Joe Bloggs</name>
59
+ <current-location>Under a tree</current-location>
60
+ <pets type="array">
61
+ <pet>
62
+ <restful-url type="link">http://example.com:3000/pets/1</restful-url>
63
+ <person-restful-url type="link">http://example.com:3000/people/1</person-restful-url>
64
+ <name nil="true"></name>
65
+ </pet>
66
+ </pets>
67
+ <sex>
68
+ <restful-url type="link">http://example.com:3000/sexes/1</restful-url>
69
+ <sex>male</sex>
70
+ </sex>
71
+ </person>
72
+
73
+
74
+ Atom-like
75
+ =========
76
+
77
+ `Person.last.to_restful.serialize(:atom_like)` OR
78
+ `Person.last.to_restful_atom_like` results in something like...
79
+
80
+ <?xml version="1.0" encoding="UTF-8"?>
81
+ <person xml:base="http://example.com:3000">
82
+ <link rel="self" href="/people/1"/>
83
+ <name>Joe Bloggs</name>
84
+ <current-location>Under a tree</current-location>
85
+ <pets>
86
+ <pet>
87
+ <link rel="self" href="/pets/1"/>
88
+ <link rel="person_id" href="/people/1"/>
89
+ <name></name>
90
+ </pet>
91
+ </pets>
92
+ <sex>
93
+ <link rel="self" href="/sexes/1"/>
94
+ <sex>male</sex>
95
+ </sex>
96
+ </person>
97
+
98
+ Params-like
99
+ ===========
100
+
101
+ `Person.last.to_restful.serialize(:params)` OR
102
+ `Person.last.to_restful_params` results in something like...
103
+
104
+ {:sex_attributes => {:sex=>"male"},
105
+ :current_location=>"Under a tree",
106
+ :name=>"Joe Bloggs",
107
+ :pets_attributes=> [ {:person_id=>1, :name=>nil} ]
108
+ }
109
+
110
+ Other Serializers
111
+ =================
112
+
113
+ Hash. Spits out a plain ole hash, no nested attributes or such like. Useful for further conversions.
114
+
115
+ Deserializing
116
+ =============
117
+
118
+ 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.
119
+
120
+ Nested Attributes
121
+ =================
122
+ 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
123
+ `accepts_nested_attributes_for :<table name>` set accordingly.
data/Rakefile ADDED
@@ -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
data/TODO.markdown ADDED
@@ -0,0 +1,9 @@
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
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
data/lib/restful.rb ADDED
@@ -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 = :hash
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,131 @@
1
+ #
2
+ # Converts an ActiveRecord model into an ApiModel
3
+ #
4
+ module Restful
5
+ module Converters
6
+ class ActiveRecord
7
+ def self.convert(model, config, options = {})
8
+ published = []
9
+ nested = config.nested?
10
+
11
+ resource = Restful.resource(
12
+ model.class.to_s.tableize.demodulize.singularize, {
13
+ :base => Restful::Rails.api_hostname,
14
+ :path => model.restful_path,
15
+ :url => model.restful_url
16
+ })
17
+
18
+ # simple attributes
19
+ resource.values += Restful::Rails.tools.simple_attributes_on(model).map do |key, value|
20
+ convert_to_simple_attribute(key, value, config, published, model)
21
+ end.compact
22
+
23
+ # has_many, has_one
24
+ resource.values += model.class.reflections.keys.map do |key|
25
+ if config.published?(key.to_sym)
26
+
27
+ # grab the associated resource(s) and run them through conversion
28
+ nested_config = config.nested(key.to_sym)
29
+ published << key.to_sym
30
+
31
+ if model.class.reflections[key].macro == :has_many && !nested
32
+ convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
33
+ Restful.collection(key, resources, extended_type)
34
+ end
35
+ elsif model.class.reflections[key].macro == :has_one or model.class.reflections[key].macro == :belongs_to
36
+
37
+ if(config.expanded?(key, nested))
38
+ convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
39
+ returning(resources.first) do |res|
40
+ res.name = key
41
+ end
42
+ end
43
+ else
44
+
45
+ value = model.send(key)
46
+ restful_path = value ? value.restful_path : nil
47
+ basename = value ? Restful::Rails.api_hostname : nil
48
+
49
+ Restful.link("#{ key }-restful-url", basename, restful_path, compute_extended_type(model, key))
50
+ end
51
+ end
52
+ end
53
+ end.compact
54
+
55
+ # Links
56
+ if model.class.apiable_association_table
57
+ resource.values += model.class.apiable_association_table.keys.map do |key|
58
+ if config.published?(key.to_sym)
59
+ published << key.to_sym
60
+ base, path = model.resolve_association_restful_url(key)
61
+ Restful.link(key.to_sym, base, path, compute_extended_type(model, key))
62
+ end
63
+ end.compact
64
+ end
65
+
66
+ # public methods
67
+ resource.values += (model.public_methods - Restful::Rails.tools.simple_attributes_on(model).keys.map(&:to_s)).map do |method_name|
68
+ if config.published?(method_name.to_sym) and not published.include?(method_name.to_sym)
69
+ value = model.send(method_name.to_sym)
70
+ sanitized_method_name = method_name.tr("!?", "").tr("_", "-").to_sym
71
+
72
+ if value.is_a? ::ActiveRecord::Base
73
+ if config.expanded?(method_name.to_sym, nested)
74
+ returning Restful::Rails.tools.expand(value, config.nested(method_name.to_sym)) do |expanded|
75
+ expanded.name = sanitized_method_name
76
+ end
77
+ else
78
+ Restful.link("#{ sanitized_method_name }-restful-url", Restful::Rails.api_hostname, value ? value.restful_path : "", compute_extended_type(model, key))
79
+ end
80
+ else
81
+ Restful.attr(sanitized_method_name, value, compute_extended_type(model, method_name))
82
+ end
83
+ end
84
+ end.compact
85
+
86
+ resource
87
+ end
88
+
89
+ def self.convert_to_simple_attribute(key, value, config, published, model = nil)
90
+ if config.published?(key.to_sym)
91
+ published << key.to_sym
92
+ ext_type = (model ? compute_extended_type(model, key) : value.class.to_s.underscore.to_sym)
93
+ Restful.attr(key.to_sym, value, ext_type)
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def self.convert_to_collection(model, key, nested_config, published)
100
+ if resources = Restful::Rails.tools.convert_collection_to_resources(model, key, nested_config)
101
+ yield key.to_sym, resources, compute_extended_type(model, key)
102
+ else
103
+ published << key.to_sym
104
+ Restful.attr(key.to_sym, nil, :notype)
105
+ end
106
+ end
107
+
108
+ def self.compute_extended_type(record, attribute_name)
109
+ type_symbol = :yaml if record.class.serialized_attributes.has_key?(attribute_name)
110
+
111
+ if column = record.class.columns_hash[attribute_name]
112
+ type_symbol = column.send(:simplified_type, column.sql_type)
113
+ else
114
+
115
+ type_symbol = record.send(attribute_name).class.to_s.underscore.to_sym
116
+ end
117
+
118
+ case type_symbol
119
+ when :text
120
+ :string
121
+ when :time
122
+ :datetime
123
+ when :date
124
+ :date
125
+ else
126
+ type_symbol
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end