restrack 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +40 -32
- data/bin/restrack +1 -1
- data/lib/restrack/resource_controller.rb +3 -1
- data/lib/restrack/resource_request.rb +15 -14
- data/lib/restrack/version.rb +1 -1
- data/test/sample_app_1/config/constants.yaml +3 -3
- data/test/sample_app_1/controllers/foo_bar_controller.rb +23 -1
- data/test/sample_app_1/test/test_controller_inputs.rb +52 -2
- data/test/sample_app_1/test/test_formats.rb +7 -8
- data/test/sample_app_3/config/constants.yaml +2 -2
- metadata +2 -2
data/README.rdoc
CHANGED
@@ -3,8 +3,7 @@
|
|
3
3
|
== Description:
|
4
4
|
RESTRack is a Rack based MVC framework that makes it extremely easy to develop RESTful data services. It is inspired
|
5
5
|
by Rails, and follows a few of its conventions. But it has no routes file, routing relationships are done through
|
6
|
-
supplying custom code blocks to class methods such as 'has_relationship_to'
|
7
|
-
'has_direct_relationship_to', and 'has_direct_relationships_to'.
|
6
|
+
supplying custom code blocks to class methods such as 'has_relationship_to' or 'has_mapped_relationships_to'.
|
8
7
|
RESTRack aims at being lightweight and easy to use. It will automatically render JSON and XML for the data
|
9
8
|
structures you return in your actions (any structure parsable by the 'json' and 'xml-simple' gems, respectively).
|
10
9
|
If you supply a view for a controller action, you do that using a builder file. Builder files are stored in the
|
@@ -55,34 +54,39 @@
|
|
55
54
|
|
56
55
|
An open, or pass-through, path can be defined via the 'pass_through_to' class method for resource controllers. This
|
57
56
|
exposes URL patterns like the following:
|
58
|
-
GET /foo/123/bar/234
|
59
|
-
GET /foo/123/bar
|
57
|
+
GET /foo/123/bar/234 <= simple pass-through from Foo 123 to show Bar 234
|
58
|
+
GET /foo/123/bar <= simple pass-through from Foo 123 to Bar index
|
60
59
|
|
61
60
|
A direct path to a single related resource's controller can be defined with the 'has_relationship_to' method. This
|
62
61
|
allows you to define a one-to-one relationship from this resource to a related resource, which means that the id of
|
63
|
-
the related resource is implied through the id of the caller. The caller has one
|
64
|
-
|
65
|
-
|
62
|
+
the related resource is implied through the id of the caller. The caller has one relation through a custom code block
|
63
|
+
passed to 'has_relationship_to'. The code block takes the caller resource's id and evaluates to the relation
|
64
|
+
resource's id, for example a PeopleController might define a one-to-one relationship like so:
|
66
65
|
has_relationship_to( :people, :as spouse ) do |id|
|
67
66
|
People.find(id).spouse.id
|
68
67
|
end
|
69
68
|
This exposes URL patterns like
|
70
69
|
the following:
|
71
|
-
GET
|
72
|
-
PUT
|
73
|
-
POST
|
70
|
+
GET /people/Sally/spouse <= direct route to show Sally's spouse
|
71
|
+
PUT /people/Henry/spouse <= direct route to update Henry's spouse
|
72
|
+
POST /people/Jane/spouse <= direct route to add Jane's spouse
|
74
73
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
has_direct_relationships_to( :people, :as => children ) do |id|
|
74
|
+
A direct path to many related resources' controller can be defined with the 'has_relationships_to' and
|
75
|
+
'has_defined_relationships_to' methods. These allows you to define one-to-many relationships. They work similar to
|
76
|
+
'has_relationship_to', except that they accept code blocks which evaluate to arrays of related child ids. Each
|
77
|
+
resource in the parent's relation list is then accessed through its array index (zero-based) in the URL. An example
|
78
|
+
of exposing the list of a People resource's children in this manner follows:
|
79
|
+
has_relationships_to( :people, :as => children ) do |id|
|
82
80
|
People.find(id).children.collect {|child| child.id}
|
83
81
|
end
|
84
|
-
GET
|
85
|
-
DELETE
|
82
|
+
GET /people/Nancy/children/0 <= direct route to show child 0
|
83
|
+
DELETE /people/Robert/children/100 <= direct route to destroy child 100
|
84
|
+
|
85
|
+
has_defined_relationships_to( :people, :as => children ) do |id|
|
86
|
+
People.find(id).children.collect {|child| child.id}
|
87
|
+
end
|
88
|
+
GET /people/Nancy/children/George <= direct route to show child 0
|
89
|
+
DELETE /people/Robert/children/John <= direct route to destroy child 100
|
86
90
|
|
87
91
|
Multiple named one-to-many relationships can be exposed with the 'has_mapped_relationships_to' method. This allows
|
88
92
|
you to define many named or keyword paths to related resources. The method's code block should accepts the parent id
|
@@ -97,17 +101,17 @@
|
|
97
101
|
}
|
98
102
|
end
|
99
103
|
This would expose the following URL patterns:
|
100
|
-
GET
|
101
|
-
PUT
|
102
|
-
POST
|
103
|
-
DELETE
|
104
|
+
GET /people/Fred/people/father => show the father of Fred
|
105
|
+
PUT /people/Fred/people/assistant => update Fred's assistant
|
106
|
+
POST /people/Fred/people/boss => add Fred's boss
|
107
|
+
DELETE /people/Luke/people/mother => destroy Luke's father
|
104
108
|
|
105
109
|
|
106
110
|
Resource id data types can be defined with the keyed_with_type class method within resource controllers. The
|
107
111
|
default data type of String is used if a different type is not specified.
|
108
112
|
|
109
113
|
|
110
|
-
==
|
114
|
+
== Miscellaneous Details
|
111
115
|
|
112
116
|
=== Logging/Logging Level
|
113
117
|
RESTRack logs to two logs, the standard log (or error log) and the request log. Paths and logging levels for these
|
@@ -125,16 +129,20 @@
|
|
125
129
|
Custom XML serialization can be done by providing Builder gem templates in views/<controller>/<action>.xml.builder
|
126
130
|
|
127
131
|
=== Inputs
|
128
|
-
====
|
132
|
+
==== Query string parameters
|
133
|
+
Available to controllers in the @params instance variable.
|
129
134
|
==== POST data
|
130
|
-
|
131
|
-
===
|
132
|
-
|
133
|
-
|
134
|
-
...yet to be done...
|
135
|
-
=== TODO: Authentication/Authorization Suggestions
|
136
|
-
...yet to be done...
|
135
|
+
Available to controllers in the @input instance variable.
|
136
|
+
=== :DEFAULT_RESOURCE
|
137
|
+
Set this option in config/constants.yaml to use an implied root resource controller.
|
138
|
+
:DEFAULT_RESOURCE: foo # /foo/123 could be accessed with /123, /foo could be accessed with /
|
137
139
|
|
140
|
+
=== :ROOT_RESOURCE_ACCEPT / :ROOT_RESOURCE_DENY
|
141
|
+
:ROOT_RESOURCE_ACCEPT: [ 'foo', 'bar' ] # OPTIONAL
|
142
|
+
defines an array of resources that can be accessed (without being proxied through another relation).
|
143
|
+
:ROOT_RESOURCE_DENY: [ 'baz' ] # OPTIONAL
|
144
|
+
defines an array of resources that cannot be accessed without proxying though another controller.
|
145
|
+
|
138
146
|
|
139
147
|
== License:
|
140
148
|
|
data/bin/restrack
CHANGED
@@ -14,7 +14,7 @@ when :generate, :gen, :g
|
|
14
14
|
puts "Generating new RESTRack service #{name}..."
|
15
15
|
RESTRack::Generator.generate_service( name )
|
16
16
|
when :controller, :cont, :c
|
17
|
-
predicate = ARGV[3].to_sym
|
17
|
+
predicate = ARGV[3] ? ARGV[3].to_sym : nil
|
18
18
|
case predicate
|
19
19
|
when :descendant_from, :parent
|
20
20
|
parent = ARGV[4]
|
@@ -20,6 +20,9 @@ module RESTRack
|
|
20
20
|
end
|
21
21
|
def __init(resource_request)
|
22
22
|
@resource_request = resource_request
|
23
|
+
@request = @resource_request.request
|
24
|
+
@params = @resource_request.params
|
25
|
+
@input = @resource_request.input
|
23
26
|
self
|
24
27
|
end
|
25
28
|
|
@@ -163,7 +166,6 @@ module RESTRack
|
|
163
166
|
def self.format_string_id(id)
|
164
167
|
return nil unless id
|
165
168
|
# default key type of resources is String
|
166
|
-
# TODO: Should this be set by service in config/constants.yaml?
|
167
169
|
self.key_type ||= String
|
168
170
|
unless self.key_type.blank? or self.key_type.ancestors.include?(String)
|
169
171
|
if self.key_type.ancestors.include?(Integer)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module RESTRack
|
2
2
|
# The ResourceRequest class handles all incoming requests.
|
3
3
|
class ResourceRequest
|
4
|
-
attr_reader :request, :request_id, :input
|
4
|
+
attr_reader :request, :request_id, :input, :params
|
5
5
|
attr_accessor :mime_type, :url_chain
|
6
6
|
|
7
7
|
# Initialize the ResourceRequest by assigning a request_id and determining the path, format, and controller of the resource.
|
@@ -13,7 +13,8 @@ module RESTRack
|
|
13
13
|
RESTRack.request_log.info "{#{@request_id}} #{@request.path_info} requested from #{@request.ip}"
|
14
14
|
RESTRack.log.debug "{#{@request_id}} Reading POST Input"
|
15
15
|
# Pull input data from POST body
|
16
|
-
@input =
|
16
|
+
@input = parse_body( @request )
|
17
|
+
@params = get_params( @request )
|
17
18
|
# Setup up the initial routing.
|
18
19
|
@url_chain = @request.path_info.split('/')
|
19
20
|
@url_chain.shift if @url_chain[0] == ''
|
@@ -66,25 +67,25 @@ module RESTRack
|
|
66
67
|
end
|
67
68
|
|
68
69
|
# Pull input data from POST body
|
69
|
-
def
|
70
|
-
input =
|
71
|
-
|
72
|
-
input = request.body.read
|
73
|
-
else
|
70
|
+
def parse_body(request)
|
71
|
+
input = request.body.read
|
72
|
+
unless request.content_type.blank?
|
74
73
|
request_mime_type = MIME::Type.new( request.content_type )
|
75
74
|
if request_mime_type.like?( RESTRack.mime_type_for( :JSON ) )
|
76
|
-
input = JSON.parse(
|
75
|
+
input = JSON.parse( input )
|
77
76
|
elsif request_mime_type.like?( RESTRack.mime_type_for( :XML ) )
|
78
|
-
input = XmlSimple.xml_in(
|
77
|
+
input = XmlSimple.xml_in( input )
|
79
78
|
elsif request_mime_type.like?( RESTRack.mime_type_for( :YAML ) )
|
80
|
-
input = YAML.parse(
|
81
|
-
else
|
82
|
-
input = request.body.read
|
79
|
+
input = YAML.parse( input )
|
83
80
|
end
|
84
|
-
RESTRack.request_log.debug "{#{@request_id}} #{request_mime_type.to_s} data in\n" + input.to_json
|
85
81
|
end
|
82
|
+
RESTRack.request_log.debug "{#{@request_id}} #{request_mime_type.to_s} data in\n" + input.pretty_inspect
|
86
83
|
input
|
87
84
|
end
|
85
|
+
|
86
|
+
def get_params(request)
|
87
|
+
params = request.GET
|
88
|
+
end
|
88
89
|
|
89
90
|
# Determine the MIME type of the request from the extension provided.
|
90
91
|
def get_mime_type_from(extension)
|
@@ -119,7 +120,7 @@ module RESTRack
|
|
119
120
|
if File.exists? builder_file
|
120
121
|
@output = builder_up(data)
|
121
122
|
else
|
122
|
-
@output = XmlSimple.xml_out(data, 'AttrPrefix' => true, 'XmlDeclaration' => true)
|
123
|
+
@output = XmlSimple.xml_out(data, 'AttrPrefix' => true, 'XmlDeclaration' => true, 'NoIndent' => true)
|
123
124
|
end
|
124
125
|
elsif @mime_type.like?(RESTRack.mime_type_for( :YAML ) )
|
125
126
|
@output = YAML.dump(data)
|
data/lib/restrack/version.rb
CHANGED
@@ -17,9 +17,9 @@
|
|
17
17
|
:DEFAULT_FORMAT: :JSON
|
18
18
|
# The resource which will handle root level requests where the name is not specified. Best for users of this not to implement method_missing in their default controller, unless they are checking for bad URI.
|
19
19
|
# This setting ('bazu') won't work because of :ROOT_RESOURCE_ACCEPT VALUE (:DEFAULT_RESOURCE should be a member of :ROOT_RESOURCE_ACCEPT).
|
20
|
-
:DEFAULT_RESOURCE:
|
20
|
+
:DEFAULT_RESOURCE: bazu
|
21
21
|
|
22
22
|
# These are the resources which can be accessed from the root of your web service. If left empty, all resources are available at the root.
|
23
|
-
:ROOT_RESOURCE_ACCEPT: [
|
23
|
+
:ROOT_RESOURCE_ACCEPT: [ foo_bar ]
|
24
24
|
# These are the resources which cannot be accessed from the root of your web service. Use either this or ROOT_RESOURCE_ACCEPT as a blacklist or whitelist to establish routing (relationships defined in resource controllers define further routing).
|
25
|
-
:ROOT_RESOURCE_DENY: [
|
25
|
+
:ROOT_RESOURCE_DENY: [ baz ]
|
@@ -78,8 +78,30 @@ class SampleApp::FooBarController < RESTRack::ResourceController
|
|
78
78
|
{ :success => true }
|
79
79
|
end
|
80
80
|
|
81
|
+
def complex_show_xml_no_builder(id)
|
82
|
+
if id == '1234567890'
|
83
|
+
return { :foo => 'abc', :bar => '123', 'baz' => 456, :more => { :one => 1, :two => [1,2], :three => :deep_fu } }
|
84
|
+
end
|
85
|
+
if id == '42'
|
86
|
+
return {
|
87
|
+
:foo => 'abc',
|
88
|
+
:bar => 123,
|
89
|
+
:baz => {
|
90
|
+
'one' => [1],
|
91
|
+
'two' => ['1','2'],
|
92
|
+
'three' => ['1', 2, {:three => 3}],
|
93
|
+
4 => :four
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
81
99
|
def echo
|
82
|
-
return @
|
100
|
+
return @input
|
101
|
+
end
|
102
|
+
|
103
|
+
def echo_get
|
104
|
+
return @params.merge({ 'get?' => @resource_request.request.get?.to_s })
|
83
105
|
end
|
84
106
|
|
85
107
|
def custom_entity(id)
|
@@ -10,6 +10,30 @@ class SampleApp::TestControllerInputs < Test::Unit::TestCase
|
|
10
10
|
@ws = SampleApp::WebService.new
|
11
11
|
end
|
12
12
|
|
13
|
+
def test_get_params
|
14
|
+
env = Rack::MockRequest.env_for('/foo_bar/echo_get?test=1&hello=world', {
|
15
|
+
:method => 'GET'
|
16
|
+
})
|
17
|
+
output = ''
|
18
|
+
assert_nothing_raised do
|
19
|
+
output = @ws.call(env)
|
20
|
+
end
|
21
|
+
test_val = { :test => '1', :hello => 'world', 'get?' => 'true' }.to_json
|
22
|
+
assert_equal test_val, output[2]
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_FUBAR_params
|
26
|
+
env = Rack::MockRequest.env_for('/foo_bar/echo_get?test=1&hello=world', {
|
27
|
+
:method => 'DELETE'
|
28
|
+
})
|
29
|
+
output = ''
|
30
|
+
assert_nothing_raised do
|
31
|
+
output = @ws.call(env)
|
32
|
+
end
|
33
|
+
test_val = { :test => '1', :hello => 'world', 'get?' => 'false' }.to_json
|
34
|
+
assert_equal test_val, output[2]
|
35
|
+
end
|
36
|
+
|
13
37
|
def test_post_no_content_type
|
14
38
|
test_val = "random text" # will be converted to json because of default response type
|
15
39
|
env = Rack::MockRequest.env_for('/foo_bar/echo', {
|
@@ -36,6 +60,32 @@ class SampleApp::TestControllerInputs < Test::Unit::TestCase
|
|
36
60
|
end
|
37
61
|
assert_equal test_val, output[2]
|
38
62
|
end
|
39
|
-
|
40
|
-
|
63
|
+
|
64
|
+
def test_post_xml
|
65
|
+
test_val = XmlSimple.xml_out({ :echo => 'niner' }, 'AttrPrefix' => true, 'XmlDeclaration' => true, 'NoIndent' => true)
|
66
|
+
env = Rack::MockRequest.env_for('/foo_bar/echo.xml', {
|
67
|
+
:method => 'POST',
|
68
|
+
:input => test_val,
|
69
|
+
'CONTENT_TYPE' => 'application/xml'
|
70
|
+
})
|
71
|
+
output = ''
|
72
|
+
assert_nothing_raised do
|
73
|
+
output = @ws.call(env)
|
74
|
+
end
|
75
|
+
assert_equal test_val, output[2]
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_post_text
|
79
|
+
test_val = 'OPCODE=PEBKAC'
|
80
|
+
env = Rack::MockRequest.env_for('/foo_bar/echo.txt', {
|
81
|
+
:method => 'POST',
|
82
|
+
:input => test_val,
|
83
|
+
'CONTENT_TYPE' => 'text/plain'
|
84
|
+
})
|
85
|
+
output = ''
|
86
|
+
assert_nothing_raised do
|
87
|
+
output = @ws.call(env)
|
88
|
+
end
|
89
|
+
assert_equal test_val, output[2]
|
90
|
+
end
|
41
91
|
end
|
@@ -30,7 +30,7 @@ class SampleApp::TestFormats < Test::Unit::TestCase
|
|
30
30
|
assert_nothing_raised do
|
31
31
|
output = @ws.call(env)
|
32
32
|
end
|
33
|
-
test_val = XmlSimple.xml_out([1,2,3,4,5,6,7], 'AttrPrefix' => true, 'XmlDeclaration' => true)
|
33
|
+
test_val = XmlSimple.xml_out([1,2,3,4,5,6,7], 'AttrPrefix' => true, 'XmlDeclaration' => true, 'NoIndent' => true)
|
34
34
|
assert_equal test_val, output[2]
|
35
35
|
|
36
36
|
env = Rack::MockRequest.env_for('/foo_bar.xml', {
|
@@ -40,7 +40,7 @@ class SampleApp::TestFormats < Test::Unit::TestCase
|
|
40
40
|
assert_nothing_raised do
|
41
41
|
output = @ws.call(env)
|
42
42
|
end
|
43
|
-
test_val = XmlSimple.xml_out([1,2,3,4,5,6,7], 'AttrPrefix' => true, 'XmlDeclaration' => true)
|
43
|
+
test_val = XmlSimple.xml_out([1,2,3,4,5,6,7], 'AttrPrefix' => true, 'XmlDeclaration' => true, 'NoIndent' => true)
|
44
44
|
assert_equal test_val, output[2]
|
45
45
|
end
|
46
46
|
|
@@ -88,25 +88,24 @@ class SampleApp::TestFormats < Test::Unit::TestCase
|
|
88
88
|
end
|
89
89
|
|
90
90
|
def test_complex_data_structure_xml
|
91
|
-
|
92
|
-
env = Rack::MockRequest.env_for('/foo_bar/1234567890.xml', {
|
91
|
+
env = Rack::MockRequest.env_for('/foo_bar/1234567890/complex_show_xml_no_builder.xml', {
|
93
92
|
:method => 'GET'
|
94
93
|
})
|
95
94
|
output = ''
|
96
95
|
assert_nothing_raised do
|
97
96
|
output = @ws.call(env)
|
98
97
|
end
|
99
|
-
test_val = "<?xml version
|
98
|
+
test_val = "<?xml version='1.0' standalone='yes'?>\n<opt><foo>abc</foo><bar>123</bar><baz>456</baz><more><one>1</one><two>1</two><two>2</two><three>deep_fu</three></more></opt>"
|
100
99
|
assert_equal test_val, output[2]
|
101
100
|
|
102
|
-
env = Rack::MockRequest.env_for('/foo_bar/42.xml', {
|
101
|
+
env = Rack::MockRequest.env_for('/foo_bar/42/complex_show_xml_no_builder.xml', {
|
103
102
|
:method => 'GET'
|
104
103
|
})
|
105
104
|
output = ''
|
106
105
|
assert_nothing_raised do
|
107
106
|
output = @ws.call(env)
|
108
107
|
end
|
109
|
-
test_val = {
|
108
|
+
test_val = XmlSimple.xml_out({
|
110
109
|
:foo => 'abc',
|
111
110
|
:bar => 123,
|
112
111
|
:baz => {
|
@@ -115,7 +114,7 @@ class SampleApp::TestFormats < Test::Unit::TestCase
|
|
115
114
|
'three' => ['1', 2, {:three => 3}],
|
116
115
|
4 => :four
|
117
116
|
}
|
118
|
-
}
|
117
|
+
}, 'AttrPrefix' => true, 'XmlDeclaration' => true, 'NoIndent' => true)
|
119
118
|
assert_equal test_val, output[2]
|
120
119
|
end
|
121
120
|
|
@@ -16,9 +16,9 @@
|
|
16
16
|
# Supported formats are :JSON, :XML, :YAML, :BIN, :TEXT
|
17
17
|
:DEFAULT_FORMAT: :JSON
|
18
18
|
# The resource which will handle root level requests where the name is not specified. Best for users of this not to implement method_missing in their default controller, unless they are checking for bad URI.
|
19
|
-
:DEFAULT_RESOURCE:
|
19
|
+
:DEFAULT_RESOURCE: bazu
|
20
20
|
|
21
21
|
# These are the resources which can be accessed from the root of your web service. If left empty, all resources are available at the root.
|
22
|
-
:ROOT_RESOURCE_ACCEPT: [
|
22
|
+
:ROOT_RESOURCE_ACCEPT: [ baz ]
|
23
23
|
# These are the resources which cannot be accessed from the root of your web service. Use either this or ROOT_RESOURCE_ACCEPT as a blacklist or whitelist to establish routing (relationships defined in resource controllers define further routing).
|
24
24
|
#:ROOT_RESOURCE_DENY: []
|