restrack 0.0.6 → 0.1.0
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/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: []
|