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