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.
- data/LICENSE.markdown +22 -0
- data/README.markdown +108 -0
- data/Rakefile +22 -0
- data/TODO.markdown +7 -0
- data/init.rb +1 -0
- data/lib/restful/apimodel/attribute.rb +17 -0
- data/lib/restful/apimodel/collection.rb +14 -0
- data/lib/restful/apimodel/link.rb +21 -0
- data/lib/restful/apimodel/resource.rb +47 -0
- data/lib/restful/converters/active_record.rb +128 -0
- data/lib/restful/rails/action_controller.rb +14 -0
- data/lib/restful/rails/active_record/configuration.rb +114 -0
- data/lib/restful/rails/active_record/metadata_tools.rb +106 -0
- data/lib/restful/rails.rb +22 -0
- data/lib/restful/serializers/atom_like_serializer.rb +51 -0
- data/lib/restful/serializers/base.rb +41 -0
- data/lib/restful/serializers/hash_serializer.rb +45 -0
- data/lib/restful/serializers/json_serializer.rb +20 -0
- data/lib/restful/serializers/params_serializer.rb +40 -0
- data/lib/restful/serializers/xml_serializer.rb +146 -0
- data/lib/restful.rb +64 -0
- data/rails/init.rb +1 -0
- data/restful.gemspec +17 -0
- data/test/converters/active_record_converter_test.rb +108 -0
- data/test/fixtures/models/person.rb +17 -0
- data/test/fixtures/models/pet.rb +5 -0
- data/test/fixtures/models/wallet.rb +5 -0
- data/test/fixtures/people.json.yaml +16 -0
- data/test/fixtures/people.xml.yaml +105 -0
- data/test/fixtures/pets.xml.yaml +21 -0
- data/test/rails/active_record_metadata_test.rb +23 -0
- data/test/rails/configuration_test.rb +45 -0
- data/test/rails/restful_publish_test.rb +41 -0
- data/test/serializers/atom_serializer_test.rb +33 -0
- data/test/serializers/json_serializer_test.rb +21 -0
- data/test/serializers/params_serializer_test.rb +44 -0
- data/test/serializers/xml_serializer_test.rb +38 -0
- data/test/test_helper.rb +129 -0
- metadata +93 -0
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,108 @@
|
|
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 => { :expanded => [:pets, :wallet] }
|
35
|
+
restful_publish :name, :pets, :restful_options => { :pets_page => 1, :pets_per_page => 100, :collapsed => :pets }
|
36
|
+
|
37
|
+
# Pet
|
38
|
+
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.
|
39
|
+
|
40
|
+
Rails-like
|
41
|
+
==========
|
42
|
+
|
43
|
+
This format sticks to xml_simple, adding links as `<association-name-restful-url>` nodes of type "link".
|
44
|
+
|
45
|
+
`Person.last.to_restful.serialize(:xml)` results in something like...
|
46
|
+
|
47
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
48
|
+
<person>
|
49
|
+
<restful-url type="link">http://example.com:3000/people/1</restful-url>
|
50
|
+
<name>Joe Bloggs</name>
|
51
|
+
<current-location>Under a tree</current-location>
|
52
|
+
<pets type="array">
|
53
|
+
<pet>
|
54
|
+
<restful-url type="link">http://example.com:3000/pets/1</restful-url>
|
55
|
+
<person-restful-url type="link">http://example.com:3000/people/1</person-restful-url>
|
56
|
+
<name nil="true"></name>
|
57
|
+
</pet>
|
58
|
+
</pets>
|
59
|
+
<sex>
|
60
|
+
<restful-url type="link">http://example.com:3000/sexes/1</restful-url>
|
61
|
+
<sex>male</sex>
|
62
|
+
</sex>
|
63
|
+
</person>
|
64
|
+
|
65
|
+
|
66
|
+
Atom-like
|
67
|
+
=========
|
68
|
+
|
69
|
+
`Person.last.to_restful.serialize(:atom_like)` results in something like...
|
70
|
+
|
71
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
72
|
+
<person xml:base="http://example.com:3000">
|
73
|
+
<link rel="self" href="/people/1"/>
|
74
|
+
<name>Joe Bloggs</name>
|
75
|
+
<current-location>Under a tree</current-location>
|
76
|
+
<pets>
|
77
|
+
<pet>
|
78
|
+
<link rel="self" href="/pets/1"/>
|
79
|
+
<link rel="person_id" href="/people/1"/>
|
80
|
+
<name></name>
|
81
|
+
</pet>
|
82
|
+
</pets>
|
83
|
+
<sex>
|
84
|
+
<link rel="self" href="/sexes/1"/>
|
85
|
+
<sex>male</sex>
|
86
|
+
</sex>
|
87
|
+
</person>
|
88
|
+
|
89
|
+
Params-like
|
90
|
+
===========
|
91
|
+
|
92
|
+
`Person.last.to_restful.serialize(:params)` results in something like...
|
93
|
+
|
94
|
+
{:sex_attributes => {:sex=>"male"},
|
95
|
+
:current_location=>"Under a tree",
|
96
|
+
:name=>"Joe Bloggs",
|
97
|
+
:pets_attributes=> [ {:person_id=>1, :name=>nil} ]
|
98
|
+
}
|
99
|
+
|
100
|
+
Deserializing
|
101
|
+
=============
|
102
|
+
|
103
|
+
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.
|
104
|
+
|
105
|
+
Nested Attributes
|
106
|
+
=================
|
107
|
+
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
|
108
|
+
`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,7 @@
|
|
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 serialzation here and test resource directly (in active_record_converter_test)
|
7
|
+
* get rid of to_a warning
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
@@ -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,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,47 @@
|
|
1
|
+
#
|
2
|
+
# Resource model. Something like a DOM model for the api.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module ApiModel
|
6
|
+
class Resource
|
7
|
+
attr_accessor :base, :path, :url, :values, :name, :type
|
8
|
+
|
9
|
+
def initialize(name, url)
|
10
|
+
self.url = url[:url]
|
11
|
+
self.path = url[:path]
|
12
|
+
self.base = url[:base]
|
13
|
+
self.name = name
|
14
|
+
self.type = :resource
|
15
|
+
self.values = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def links
|
19
|
+
values.select { |attribute| attribute.type == :link }
|
20
|
+
end
|
21
|
+
|
22
|
+
def simple_attributes
|
23
|
+
values.select { |attribute| attribute.type == :simple_attribute }
|
24
|
+
end
|
25
|
+
|
26
|
+
def collections
|
27
|
+
values.select { |attribute| attribute.type == :collection }
|
28
|
+
end
|
29
|
+
|
30
|
+
# invoke serialization
|
31
|
+
def serialize(type)
|
32
|
+
serializer = Restful::Serializers::Base.serializer(type)
|
33
|
+
serializer.serialize(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
# invoke deserialization
|
37
|
+
def deserialize_from(type)
|
38
|
+
serializer = Restful::Serializers::Base.serializer(type)
|
39
|
+
serializer.deserialize(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def full_url
|
43
|
+
base.blank? ? url : "#{ base }#{ path }"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,128 @@
|
|
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
|
+
|
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
|
+
|
20
|
+
# simple attributes
|
21
|
+
resource.values += Restful::Rails.tools.simple_attributes_on(model).map do |attribute|
|
22
|
+
key, value = attribute
|
23
|
+
|
24
|
+
if config.published?(key.to_sym)
|
25
|
+
published << key.to_sym
|
26
|
+
Restful.attr(key.to_sym, value, compute_extended_type(model, key))
|
27
|
+
end
|
28
|
+
end.compact
|
29
|
+
|
30
|
+
# has_many, has_one
|
31
|
+
resource.values += model.class.reflections.keys.map do |key|
|
32
|
+
if config.published?(key.to_sym)
|
33
|
+
|
34
|
+
# grab the associated resource(s) and run them through conversion
|
35
|
+
nested_config = config.nested(key.to_sym)
|
36
|
+
published << key.to_sym
|
37
|
+
|
38
|
+
if model.class.reflections[key].macro == :has_many && !nested
|
39
|
+
convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
|
40
|
+
Restful.collection(key, resources, extended_type)
|
41
|
+
end
|
42
|
+
elsif model.class.reflections[key].macro == :has_one or model.class.reflections[key].macro == :belongs_to
|
43
|
+
|
44
|
+
if(config.expanded? && !nested)
|
45
|
+
convert_to_collection(model, key, nested_config, published) do |key, resources, extended_type|
|
46
|
+
returning(resources.first) do |res|
|
47
|
+
res.name = key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
|
52
|
+
value = model.send(key)
|
53
|
+
restful_path = value ? value.restful_path : nil
|
54
|
+
basename = value ? Restful::Rails.api_hostname : nil
|
55
|
+
|
56
|
+
Restful.link("#{ key }-restful-url", basename, restful_path, compute_extended_type(model, key))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end.compact
|
61
|
+
|
62
|
+
# Links
|
63
|
+
if model.class.apiable_association_table
|
64
|
+
resource.values += model.class.apiable_association_table.keys.map do |key|
|
65
|
+
if config.published?(key.to_sym)
|
66
|
+
published << key.to_sym
|
67
|
+
base, path = model.resolve_association_restful_url(key)
|
68
|
+
Restful.link(key.to_sym, base, path, compute_extended_type(model, key))
|
69
|
+
end
|
70
|
+
end.compact
|
71
|
+
end
|
72
|
+
|
73
|
+
# public methods
|
74
|
+
resource.values += (model.public_methods - Restful::Rails.tools.simple_attributes_on(model).keys.map(&:to_s)).map do |method_name|
|
75
|
+
if config.published?(method_name.to_sym) and not published.include?(method_name.to_sym)
|
76
|
+
value = model.send(method_name.to_sym)
|
77
|
+
sanitzed_method_name = method_name.tr("!?", "").tr("_", "-").to_sym
|
78
|
+
|
79
|
+
if value.is_a? ::ActiveRecord::Base
|
80
|
+
if config.expanded? && !nested
|
81
|
+
returning Restful::Rails.tools.expand(value, config.nested(method_name.to_sym)) do |expanded|
|
82
|
+
expanded.name = sanitzed_method_name
|
83
|
+
end
|
84
|
+
else
|
85
|
+
Restful.link("#{ sanitzed_method_name }-restful-url", Restful::Rails.api_hostname, value ? value.restful_path : "", compute_extended_type(model, key))
|
86
|
+
end
|
87
|
+
else
|
88
|
+
Restful.attr(sanitzed_method_name, value, compute_extended_type(model, method_name))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end.compact
|
92
|
+
|
93
|
+
resource
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def self.convert_to_collection(model, key, nested_config, published)
|
99
|
+
if resources = Restful::Rails.tools.convert_collection_to_resources(model, key, nested_config)
|
100
|
+
yield key.to_sym, resources, compute_extended_type(model, key)
|
101
|
+
else
|
102
|
+
published << key.to_sym
|
103
|
+
Restful.attr(key.to_sym, nil, :notype)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.compute_extended_type(record, attribute_name)
|
108
|
+
type_symbol = :yaml if record.class.serialized_attributes.has_key?(attribute_name)
|
109
|
+
|
110
|
+
if column = record.class.columns_hash[attribute_name]
|
111
|
+
type_symbol = column.send(:simplified_type, column.sql_type)
|
112
|
+
else
|
113
|
+
|
114
|
+
type_symbol = record.send(attribute_name).class.to_s.underscore.to_sym
|
115
|
+
end
|
116
|
+
|
117
|
+
case type_symbol
|
118
|
+
when :text
|
119
|
+
:string
|
120
|
+
when :time
|
121
|
+
:datetime
|
122
|
+
else
|
123
|
+
type_symbol
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#
|
2
|
+
# Configuration options for restful.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module Rails
|
6
|
+
module ActiveRecord
|
7
|
+
module Configuration
|
8
|
+
def self.included(base)
|
9
|
+
base.send :class_inheritable_accessor, :restful_config
|
10
|
+
base.restful_config = Config.new
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
|
17
|
+
#
|
18
|
+
# In the form:
|
19
|
+
#
|
20
|
+
# Person.restful_publish(:name, :pets => [:name, :species])
|
21
|
+
#
|
22
|
+
# If pet already has configured the api with restful_publish, you would
|
23
|
+
# get the default nested attributes. In the above example, these would be
|
24
|
+
# overriden.
|
25
|
+
#
|
26
|
+
def restful_publish(*fieldnames) # declarative setter method
|
27
|
+
self.restful_config = Restful.cfg(*fieldnames)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
|
33
|
+
#
|
34
|
+
# converts this AR object to an apimodel object. per default, only the
|
35
|
+
# attributes in self.class.restful_config are shown. this can be overriden
|
36
|
+
# by passing in something like @pet.to_restful(:name, :species).
|
37
|
+
#
|
38
|
+
def to_restful(config = self.class.restful_config)
|
39
|
+
|
40
|
+
if config && !config.is_a?(Config)
|
41
|
+
config = Config.new(config)
|
42
|
+
end
|
43
|
+
|
44
|
+
config.whitelisted = self.class.restful_config.whitelisted if config.whitelisted.empty?
|
45
|
+
|
46
|
+
Restful::Converters::ActiveRecord.convert(self, config)
|
47
|
+
end
|
48
|
+
|
49
|
+
# simple method through which a model should know it's own name. override this where necessary.
|
50
|
+
def restful_url(url_base = Restful::Rails.api_hostname)
|
51
|
+
"#{ url_base }#{ restful_path }"
|
52
|
+
end
|
53
|
+
|
54
|
+
def restful_path
|
55
|
+
"/#{ self.class.to_s.tableize }/#{ self.to_param }"
|
56
|
+
end
|
57
|
+
|
58
|
+
# FIXME: read out Restful::Serializers::Base.serializers. Load order problems?
|
59
|
+
[:xml, :json, :atom_like].each do |format|
|
60
|
+
define_method("to_restful_#{ format }") do |*args|
|
61
|
+
self.to_restful(*args).serialize(format)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Config # configures what attributes are exposed to the api. for a single resource.
|
67
|
+
|
68
|
+
attr_accessor :whitelisted, :restful_options
|
69
|
+
|
70
|
+
def initialize(*fields) # set; eg :name, :pets => [:name, :species]
|
71
|
+
@whitelisted, @restful_options = split_into_whitelist_and_restful_options([fields].flatten.compact)
|
72
|
+
end
|
73
|
+
|
74
|
+
def published?(key)
|
75
|
+
@whitelisted.include?(key) || !!@whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first
|
76
|
+
end
|
77
|
+
|
78
|
+
def expanded? # if nothing was set, this defaults to true.
|
79
|
+
@restful_options[:expansion] != :collapsed
|
80
|
+
end
|
81
|
+
|
82
|
+
def nested?
|
83
|
+
!!restful_options[:nested]
|
84
|
+
end
|
85
|
+
|
86
|
+
def nested(key)
|
87
|
+
definition = @whitelisted.select { |field| field.is_a?(Hash) && field.keys.include?(key) }.first
|
88
|
+
Config.new((definition[key] if definition))
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def split_into_whitelist_and_restful_options(array)
|
94
|
+
options = {}
|
95
|
+
|
96
|
+
return array.map do |el|
|
97
|
+
if el.is_a? Hash
|
98
|
+
el = el.clone
|
99
|
+
deleted = el.delete(:restful_options)
|
100
|
+
options.merge!(deleted) if deleted
|
101
|
+
el = nil if el == {}
|
102
|
+
end
|
103
|
+
|
104
|
+
el
|
105
|
+
end.compact, options
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
ActiveRecord::Base.send :include, Restful::Rails::ActiveRecord::Configuration
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#
|
2
|
+
# Handle ActiveRecord associations and such like.
|
3
|
+
#
|
4
|
+
module Restful
|
5
|
+
module Rails
|
6
|
+
module ActiveRecord
|
7
|
+
module MetadataTools
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_inheritable_accessor :apiable_associations
|
11
|
+
base.class_inheritable_accessor :apiable_association_table
|
12
|
+
|
13
|
+
base.send :include, InstanceMethods
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def apiable
|
20
|
+
cache_association_restful_url_metadata
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_association_restful_url_metadata
|
24
|
+
self.apiable_associations ||= (self.reflect_on_all_associations(:belongs_to) + self.reflect_on_all_associations(:has_one)).flatten.uniq
|
25
|
+
self.apiable_association_table ||= self.apiable_associations.inject({}) { |memo, reflection| memo[reflection.primary_key_name] = reflection; memo }
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_by_restful(id)
|
29
|
+
find(id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
def resolve_association_restful_url(association_key_name)
|
35
|
+
self.class.cache_association_restful_url_metadata
|
36
|
+
|
37
|
+
if reflection = self.class.apiable_association_table[association_key_name]
|
38
|
+
related_resource = self.send(reflection.name)
|
39
|
+
[Restful::Rails.api_hostname, related_resource.restful_path] if related_resource
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Utils
|
45
|
+
|
46
|
+
# called for nested resources.
|
47
|
+
def self.expand(resource, config)
|
48
|
+
config.restful_options[:nested] = true
|
49
|
+
resource.to_restful(config)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.link(key, model, config)
|
53
|
+
Restful.link(key.to_sym, "base", "path", "link")
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.dereference(url)
|
57
|
+
regexp = Regexp.new("#{ Restful::Rails.api_hostname }\/(.*)\/(.*)")
|
58
|
+
m, resource, params = *url.match(regexp)
|
59
|
+
resource = if resource && params
|
60
|
+
clazz = resource.try(:singularize).try(:camelize).try(:constantize)
|
61
|
+
clazz.find_by_restful(params) if clazz
|
62
|
+
end
|
63
|
+
|
64
|
+
resource ? resource.id : 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# retruns non association / collection attributes.
|
68
|
+
def self.simple_attributes_on(model)
|
69
|
+
attributes = model.attributes
|
70
|
+
|
71
|
+
attributes.delete_if do |k, v|
|
72
|
+
model.class.apiable_association_table && model.class.apiable_association_table.keys.include?(k)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Takes an ar model and a key like :people, and returns an array of resources.
|
78
|
+
#
|
79
|
+
# TODO: don't load the entire association, only the published attributes (with an appropriate :select).
|
80
|
+
# TODO: get some pagination in.
|
81
|
+
#
|
82
|
+
def self.convert_collection_to_resources(model, key, config)
|
83
|
+
|
84
|
+
# load the associated objects.
|
85
|
+
models = model.send(key)
|
86
|
+
|
87
|
+
# convert them to_restful.
|
88
|
+
if models
|
89
|
+
[*models].map do |m|
|
90
|
+
if m.respond_to? :to_restful
|
91
|
+
config.nested? ?
|
92
|
+
link(key, m, config) :
|
93
|
+
expand(m, config)
|
94
|
+
else
|
95
|
+
raise "Seems as if you want to export the relation #{ key } of an #{ model.class.to_s } object without making #{ key } apiable."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
ActiveRecord::Base.send :include, Restful::Rails::ActiveRecord::MetadataTools
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Restful
|
2
|
+
module Rails
|
3
|
+
|
4
|
+
# sets the hostname for this request in a threadsafe manner.
|
5
|
+
def self.api_hostname=(hostname)
|
6
|
+
Thread.current[:api_hostname] = hostname
|
7
|
+
end
|
8
|
+
|
9
|
+
# gets the hostname for the currently running thread.
|
10
|
+
def self.api_hostname
|
11
|
+
Thread.current[:api_hostname]
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Shortcuts past namespaces
|
16
|
+
#
|
17
|
+
def self.tools
|
18
|
+
Restful::Rails::ActiveRecord::MetadataTools::Utils
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|