restrack 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/README.rdoc +71 -0
- data/Rakefile +32 -0
- data/bin/restrack +23 -0
- data/config/constants.yaml +8 -0
- data/lib/restrack/generator/constants.yaml.erb +24 -0
- data/lib/restrack/generator/controller.rb.erb +35 -0
- data/lib/restrack/generator/loader.rb.erb +21 -0
- data/lib/restrack/generator.rb +93 -0
- data/lib/restrack/http_status.rb +10 -0
- data/lib/restrack/resource_controller.rb +192 -0
- data/lib/restrack/resource_request.rb +135 -0
- data/lib/restrack/support.rb +56 -0
- data/lib/restrack/version.rb +3 -0
- data/lib/restrack/web_service.rb +66 -0
- data/lib/restrack.rb +24 -0
- data/restrack.gemspec +28 -0
- data/test/sample_app_1/config/constants.yaml +25 -0
- data/test/sample_app_1/controllers/bat_controller.rb +9 -0
- data/test/sample_app_1/controllers/baz_controller.rb +9 -0
- data/test/sample_app_1/controllers/baza_controller.rb +11 -0
- data/test/sample_app_1/controllers/bazu_controller.rb +16 -0
- data/test/sample_app_1/controllers/foo_bar_controller.rb +62 -0
- data/test/sample_app_1/loader.rb +31 -0
- data/test/sample_app_1/test/test_controller_actions.rb +122 -0
- data/test/sample_app_1/test/test_controller_modifiers.rb +153 -0
- data/test/sample_app_1/test/test_formats.rb +119 -0
- data/test/sample_app_1/test/test_resource_request.rb +160 -0
- data/test/sample_app_1/test/test_web_service.rb +27 -0
- data/test/sample_app_1/views/foo_bar/show.xml.builder +4 -0
- data/test/sample_app_2/config/constants.yaml +24 -0
- data/test/sample_app_2/controllers/bat_controller.rb +9 -0
- data/test/sample_app_2/controllers/baz_controller.rb +9 -0
- data/test/sample_app_2/controllers/baza_controller.rb +11 -0
- data/test/sample_app_2/controllers/bazu_controller.rb +8 -0
- data/test/sample_app_2/controllers/foo_bar_controller.rb +59 -0
- data/test/sample_app_2/loader.rb +31 -0
- data/test/sample_app_2/test/test_controller_modifiers.rb +121 -0
- data/test/sample_app_2/test/test_resource_request.rb +71 -0
- data/test/sample_app_2/views/foo_bar/show.xml.builder +4 -0
- data/test/sample_app_3/config/constants.yaml +24 -0
- data/test/sample_app_3/controllers/bat_controller.rb +9 -0
- data/test/sample_app_3/controllers/baz_controller.rb +9 -0
- data/test/sample_app_3/controllers/baza_controller.rb +11 -0
- data/test/sample_app_3/controllers/bazu_controller.rb +8 -0
- data/test/sample_app_3/controllers/foo_bar_controller.rb +59 -0
- data/test/sample_app_3/loader.rb +31 -0
- data/test/sample_app_3/test/test_resource_request.rb +42 -0
- data/test/sample_app_3/views/foo_bar/show.xml.builder +4 -0
- data/test/sample_app_4/config/constants.yaml +24 -0
- data/test/sample_app_4/controllers/bar_controller.rb +11 -0
- data/test/sample_app_4/controllers/baz_controller.rb +15 -0
- data/test/sample_app_4/controllers/foo_controller.rb +21 -0
- data/test/sample_app_4/loader.rb +31 -0
- data/test/sample_app_4/test/test_controller_modifiers.rb +28 -0
- data/test/sample_app_4/test/test_formats.rb +49 -0
- data/test/sample_app_4/views/alphatest.png +0 -0
- data/test/test_support.rb +20 -0
- data/test/test_web_service.rb +31 -0
- metadata +238 -0
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
= RESTRack
|
2
|
+
|
3
|
+
== Description:
|
4
|
+
RESTRack is an Rack-based MVC framework tailored specifically to RESTful data
|
5
|
+
services.
|
6
|
+
|
7
|
+
== Installation:
|
8
|
+
<sudo> gem install restrack
|
9
|
+
|
10
|
+
|
11
|
+
== Why RESTRack when there is Rails?
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
== Usage:
|
16
|
+
- restrack generate service foo_bar
|
17
|
+
- restrack generate controller baz
|
18
|
+
- restrack server 3456
|
19
|
+
|
20
|
+
|
21
|
+
== REST action method names
|
22
|
+
All default RESTful controller method names align with their Rails
|
23
|
+
counterparts, with more actions supported.
|
24
|
+
|
25
|
+
=== RESTRack:
|
26
|
+
# HTTP Verb: | GET | PUT | POST | DELETE
|
27
|
+
# Collection URI (/widgets/): | index | replace | create | drop
|
28
|
+
# Element URI (/widgets/42): | show | update | add | destroy
|
29
|
+
|
30
|
+
=== Rails
|
31
|
+
# HTTP Verb: | GET | PUT | POST | DELETE
|
32
|
+
# Collection URI (/widgets/): | index | | create |
|
33
|
+
# Element URI (/widgets/42): | show | update | | destroy
|
34
|
+
|
35
|
+
|
36
|
+
== How-Tos
|
37
|
+
=== Authentication
|
38
|
+
|
39
|
+
=== Logging Level
|
40
|
+
|
41
|
+
=== XML Serialization
|
42
|
+
==== With Builder
|
43
|
+
==== With XmlSimple
|
44
|
+
@output = XmlSimple.xml_out(data, 'AttrPrefix' => true)
|
45
|
+
=== Inputs
|
46
|
+
==== GET params
|
47
|
+
==== POST data
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
== License:
|
52
|
+
|
53
|
+
Copyright (c) 2010 Chris St. John
|
54
|
+
|
55
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
56
|
+
of this software and associated documentation files (the "Software"), to deal
|
57
|
+
in the Software without restriction, including without limitation the rights
|
58
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
59
|
+
copies of the Software, and to permit persons to whom the Software is
|
60
|
+
furnished to do so, subject to the following conditions:
|
61
|
+
|
62
|
+
The above copyright notice and this permission notice shall be included in
|
63
|
+
all copies or substantial portions of the Software.
|
64
|
+
|
65
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
66
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
67
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
68
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
69
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
70
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
71
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
require 'bundler'
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
task :default => [:test_all]
|
8
|
+
|
9
|
+
desc 'Run all tests.'
|
10
|
+
Rake::TestTask.new('test_all') { |t|
|
11
|
+
t.pattern = 'test/**/test_*.rb'
|
12
|
+
}
|
13
|
+
|
14
|
+
desc 'Run sample_app_1 tests.'
|
15
|
+
Rake::TestTask.new('test1') { |t|
|
16
|
+
t.pattern = 'test/sample_app_1/**/test_*.rb'
|
17
|
+
}
|
18
|
+
|
19
|
+
desc 'Run sample_app_2 tests.'
|
20
|
+
Rake::TestTask.new('test2') { |t|
|
21
|
+
t.pattern = 'test/sample_app_2/**/test_*.rb'
|
22
|
+
}
|
23
|
+
|
24
|
+
desc 'Run sample_app_3 tests.'
|
25
|
+
Rake::TestTask.new('test3') { |t|
|
26
|
+
t.pattern = 'test/sample_app_3/**/test_*.rb'
|
27
|
+
}
|
28
|
+
|
29
|
+
desc 'Run sample_app_4 tests.'
|
30
|
+
Rake::TestTask.new('test4') { |t|
|
31
|
+
t.pattern = 'test/sample_app_4/**/test_*.rb'
|
32
|
+
}
|
data/bin/restrack
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'restrack'
|
4
|
+
|
5
|
+
verb = ARGV[0]
|
6
|
+
noun = ARGV[1]
|
7
|
+
|
8
|
+
case verb.to_sym
|
9
|
+
when :generate
|
10
|
+
name = ARGV[2]
|
11
|
+
case noun.to_sym
|
12
|
+
when :service
|
13
|
+
puts "Generating new RESTRack service #{name}..."
|
14
|
+
RESTRack::Generator.generate_service( name )
|
15
|
+
when :controller
|
16
|
+
puts "Generating new controller #{name}..."
|
17
|
+
RESTRack::Generator.generate_controller( name )
|
18
|
+
end
|
19
|
+
puts 'Creation is complete.'
|
20
|
+
when :server
|
21
|
+
options = { :Port => noun || 9292, :config => 'config.ru' }
|
22
|
+
Rack::Server.start( options )
|
23
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
:LOG: '/var/log/restrack/restrack.log'
|
2
|
+
:REQUEST_LOG: '/var/log/restrack/restrack.request.log'
|
3
|
+
:LOG_LEVEL: DEBUG # Logger object level
|
4
|
+
:REQUEST_LOG_LEVEL: DEBUG # Logger object level
|
5
|
+
:DEFAULT_FORMAT: :JSON # Supported formats are :JSON, :XML, :YAML, :BIN, :TEXT
|
6
|
+
:DEFAULT_RESOURCE: '' # 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. :DEFAULT_RESOURCE should be a member of :ROOT_RESOURCE_ACCEPT.
|
7
|
+
:ROOT_RESOURCE_ACCEPT: [] # 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.
|
8
|
+
:ROOT_RESOURCE_DENY: [] # 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).
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#GENERATOR-CONST# -DO NOT REMOVE OR CHANGE THIS LINE- Application-Namespace => <%= @service_name %>
|
2
|
+
#
|
3
|
+
# = constants.yaml
|
4
|
+
# This is where RESTRack applications define the constants relevant to their particular
|
5
|
+
# application that are used by the RESTRack base classes.
|
6
|
+
|
7
|
+
# Application log path definition
|
8
|
+
:LOG: '/var/log/<%= @service_name %>/<%= @service_name %>.log'
|
9
|
+
# Request log path definition
|
10
|
+
:REQUEST_LOG: '/var/log/<%= @service_name %>/<%= @service_name %>.request.log'
|
11
|
+
|
12
|
+
# Logger object levels
|
13
|
+
:LOG_LEVEL: :DEBUG
|
14
|
+
:REQUEST_LOG_LEVEL: :DEBUG
|
15
|
+
|
16
|
+
# Supported formats are :JSON, :XML, :YAML, :BIN, :TEXT
|
17
|
+
:DEFAULT_FORMAT: :JSON
|
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: nil
|
20
|
+
|
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: []
|
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
|
+
:ROOT_RESOURCE_DENY: []
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class <%= @service_name.camelize %>::<%= @name.camelize %>Controller < RESTRack::ResourceController
|
2
|
+
|
3
|
+
def index
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
def create
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def replace
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def show(id)
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(id)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(id)
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(id)
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'restrack'
|
2
|
+
|
3
|
+
module <%= @service_name.camelize %>; end
|
4
|
+
class <%= @service_name.camelize %>::WebService < RESTRack::WebService; end
|
5
|
+
|
6
|
+
RESTRack::CONFIG = RESTRack::load_config(File.join(File.dirname(__FILE__), 'config/constants.yaml'))
|
7
|
+
RESTRack::CONFIG[:ROOT] = File.dirname(__FILE__)
|
8
|
+
|
9
|
+
# Dynamically load all controllers
|
10
|
+
Find.find( File.join(File.dirname(__FILE__), 'controllers') ) do |file|
|
11
|
+
next if File.extname(file) != '.rb'
|
12
|
+
require file
|
13
|
+
end
|
14
|
+
|
15
|
+
if File.directory?( File.join(File.dirname(__FILE__), 'models') )
|
16
|
+
# Dynamically load all models
|
17
|
+
Find.find( File.join(File.dirname(__FILE__), 'models') ) do |file|
|
18
|
+
next if File.extname(file) != '.rb'
|
19
|
+
require file
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
module RESTRack
|
7
|
+
class Generator
|
8
|
+
TEMPLATE = {
|
9
|
+
:service => 'loader.rb.erb',
|
10
|
+
:constants => 'constants.yaml.erb',
|
11
|
+
:controller => 'controller.rb.erb'
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def generate_controller(name)
|
17
|
+
# Generate controller file
|
18
|
+
template = get_template_for( :controller )
|
19
|
+
resultant_string = template.result( get_binding_for_controller( name ) )
|
20
|
+
File.open("#{base_dir}/controllers/#{name}_controller.rb", 'w') {|f| f.puts resultant_string }
|
21
|
+
# Generate view folder for controller
|
22
|
+
FileUtils.makedirs("#{name}/views")
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_service(name)
|
26
|
+
FileUtils.makedirs("#{name}/config")
|
27
|
+
FileUtils.makedirs("#{name}/controllers")
|
28
|
+
FileUtils.makedirs("#{name}/models")
|
29
|
+
FileUtils.makedirs("#{name}/test")
|
30
|
+
FileUtils.makedirs("#{name}/views")
|
31
|
+
|
32
|
+
template = get_template_for( :service )
|
33
|
+
resultant_string = template.result( get_binding_for_service( name ) )
|
34
|
+
File.open("#{name}/loader.rb", 'w') {|f| f.puts resultant_string }
|
35
|
+
|
36
|
+
template = get_template_for( :constants )
|
37
|
+
resultant_string = template.result( get_binding_for_service( name ) )
|
38
|
+
File.open("#{name}/config/constants.yaml", 'w') {|f| f.puts resultant_string }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def get_template_for(type)
|
44
|
+
template_file = File.new(File.join(File.dirname(__FILE__),"generator/#{TEMPLATE[type]}"))
|
45
|
+
template = ERB.new( template_file.read, nil, "%" )
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_binding_for_controller(name)
|
49
|
+
@name = name
|
50
|
+
@service_name = get_service_name
|
51
|
+
binding
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_binding_for_service(name)
|
55
|
+
@service_name = name
|
56
|
+
binding
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_service_name
|
60
|
+
line = ''
|
61
|
+
begin
|
62
|
+
File.open(File.join(base_dir, 'config/constants.yaml')) { |f| line = f.gets }
|
63
|
+
rescue
|
64
|
+
raise File.join(base_dir, 'config/constants.yaml') + ' not found or could not be opened!'
|
65
|
+
end
|
66
|
+
begin
|
67
|
+
check = line.match(/#GENERATOR-CONST#.*Application-Namespace\s*=>\s*(.+)/)[0]
|
68
|
+
service_name = $1
|
69
|
+
rescue
|
70
|
+
raise '#GENERATOR-CONST# line has been removed or modified in config/constants.yaml.'
|
71
|
+
end
|
72
|
+
return service_name
|
73
|
+
end
|
74
|
+
|
75
|
+
def base_dir
|
76
|
+
base_dir = nil
|
77
|
+
this_path = File.join( Dir.pwd, 'config/constants.yaml')
|
78
|
+
while this_path != '/config/constants.yaml'
|
79
|
+
if File.exists?( this_path )
|
80
|
+
base_dir = Dir.pwd
|
81
|
+
break
|
82
|
+
else
|
83
|
+
this_path = File.join('..', this_path)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
raise 'The config/constants.yaml file could not found when determining base_dir!' unless base_dir
|
87
|
+
return base_dir
|
88
|
+
end
|
89
|
+
|
90
|
+
end # class << self
|
91
|
+
|
92
|
+
end # class
|
93
|
+
end # module
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module HTTPStatus
|
2
|
+
class HTTP400BadRequest < Exception; end
|
3
|
+
class HTTP401Unauthorized < Exception; end
|
4
|
+
class HTTP403Forbidden < Exception; end
|
5
|
+
class HTTP404ResourceNotFound < Exception; end
|
6
|
+
class HTTP405MethodNotAllowed < Exception; end
|
7
|
+
class HTTP409Conflict < Exception; end
|
8
|
+
class HTTP410Gone < Exception; end
|
9
|
+
class HTTP500ServerError < Exception; end
|
10
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module RESTRack
|
2
|
+
class ResourceController
|
3
|
+
attr_reader :input, :output
|
4
|
+
|
5
|
+
def self.__init(resource_request)
|
6
|
+
# Base initialization method for resources and storage of request input
|
7
|
+
# This method should not be overriden in decendent classes.
|
8
|
+
__self = self.new
|
9
|
+
return __self.__init(resource_request)
|
10
|
+
end
|
11
|
+
def __init(resource_request)
|
12
|
+
@resource_request = resource_request
|
13
|
+
setup_action
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
# Call the controller's action and return it in the proper format.
|
19
|
+
args = []
|
20
|
+
args << @resource_request.id unless @resource_request.id.blank?
|
21
|
+
package( self.send(@resource_request.action.to_sym, *args) )
|
22
|
+
end
|
23
|
+
|
24
|
+
# HTTP Verb: | GET | PUT | POST | DELETE
|
25
|
+
# Collection URI (/widgets/): | index | replace | create | drop
|
26
|
+
# Element URI (/widgets/42): | show | update | add | destroy
|
27
|
+
|
28
|
+
#def index; end
|
29
|
+
#def replace; end
|
30
|
+
#def create; end
|
31
|
+
#def drop; end
|
32
|
+
#def show(id); end
|
33
|
+
#def update(id); end
|
34
|
+
#def add(id); end
|
35
|
+
#def destroy(id); end
|
36
|
+
|
37
|
+
def method_missing(method_sym, *arguments, &block)
|
38
|
+
raise HTTP405MethodNotAllowed, 'Method not provided on controller.'
|
39
|
+
end
|
40
|
+
|
41
|
+
protected # all internal methods are protected rather than private so that calling methods *could* be overriden if necessary.
|
42
|
+
def self.has_relationship_to(entity, opts = {})
|
43
|
+
# This method allows one to access a related resource, without providing a direct link to specific relation(s).
|
44
|
+
entity_name = opts[:as] || entity
|
45
|
+
define_method( entity_name.to_sym,
|
46
|
+
Proc.new do
|
47
|
+
@resource_request.id, @resource_request.action = nil, nil
|
48
|
+
( @resource_request.id, @resource_request.action, @resource_request.path_stack ) = @resource_request.path_stack.split('/', 3) unless @resource_request.path_stack.blank?
|
49
|
+
if [ :index, :replace, :create, :destroy ].include? @resource_request.id
|
50
|
+
@resource_request.action = @resource_request.id
|
51
|
+
@resource_request.id = nil
|
52
|
+
end
|
53
|
+
format_id
|
54
|
+
self.call_relation(entity)
|
55
|
+
end
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.has_direct_relationship_to(entity, opts = {}, &get_entity_id_from_relation_id)
|
60
|
+
# This method defines that there is a single link to a member from an entity collection.
|
61
|
+
# The second parameter is an options hash to support setting the local name of the relation via ':as => :foo'.
|
62
|
+
# The third parameter to the method is a Proc which accepts the calling entity's id and returns the id of the relation to which we're establishing the link.
|
63
|
+
# This adds an accessor instance method whose name is the entity's class.
|
64
|
+
entity_name = opts[:as] || entity
|
65
|
+
define_method( entity_name.to_sym,
|
66
|
+
Proc.new do
|
67
|
+
@resource_request.id = get_entity_id_from_relation_id.call(@resource_request.id)
|
68
|
+
@resource_request.action = nil
|
69
|
+
( @resource_request.action, @resource_request.path_stack ) = @resource_request.path_stack.split('/', 3) unless @resource_request.path_stack.blank?
|
70
|
+
format_id
|
71
|
+
self.call_relation(entity)
|
72
|
+
end
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.has_direct_relationships_to(entity, opts = {}, &get_entity_id_from_relation_id)
|
77
|
+
# This method defines that there are multiple links to members from an entity collection (an array of entity identifiers).
|
78
|
+
# This adds an accessor instance method whose name is the entity's class.
|
79
|
+
entity_name = opts[:as] || entity
|
80
|
+
define_method( entity_name.to_sym,
|
81
|
+
Proc.new do
|
82
|
+
entity_array = get_entity_id_from_relation_id.call(@resource_request.id)
|
83
|
+
@resource_request.id, @resource_request.action = nil, nil
|
84
|
+
( @resource_request.id, @resource_request.action, @resource_request.path_stack ) = @resource_request.path_stack.split('/', 3) unless @resource_request.path_stack.blank?
|
85
|
+
format_id
|
86
|
+
unless entity_array.include?( @resource_request.id )
|
87
|
+
raise HTTP404ResourceNotFound, 'Relation entity does not belong to referring resource.'
|
88
|
+
end
|
89
|
+
self.call_relation(entity)
|
90
|
+
end
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.has_mapped_relationships_to(entity, opts = {}, &get_entity_id_from_relation_id)
|
95
|
+
# This method defines that there are mapped links to members from an entity collection (a hash of entity identifiers).
|
96
|
+
# This adds an accessor instance method whose name is the entity's class.
|
97
|
+
entity_name = opts[:as] || entity
|
98
|
+
define_method( entity_name.to_sym,
|
99
|
+
Proc.new do
|
100
|
+
entity_map = get_entity_id_from_relation_id.call(@resource_request.id)
|
101
|
+
@resource_request.action = nil
|
102
|
+
( key, @resource_request.action, @resource_request.path_stack ) = @resource_request.path_stack.split('/', 3) unless @resource_request.path_stack.blank?
|
103
|
+
format_id
|
104
|
+
unless @resource_request.id = entity_map[key.to_sym]
|
105
|
+
raise HTTP404ResourceNotFound, 'Relation entity does not belong to referring resource.'
|
106
|
+
end
|
107
|
+
self.call_relation(entity)
|
108
|
+
end
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def call_relation(entity)
|
113
|
+
# Call the child relation (next entity in the path stack)
|
114
|
+
# common logic to all relationship methods
|
115
|
+
@resource_request.resource_name = entity.to_s.camelize
|
116
|
+
setup_action
|
117
|
+
@resource_request.locate
|
118
|
+
@resource_request.call
|
119
|
+
end
|
120
|
+
|
121
|
+
def setup_action
|
122
|
+
# If the action is not set with the request URI, determine the action from HTTP Verb.
|
123
|
+
if @resource_request.action.blank?
|
124
|
+
if @resource_request.request.get?
|
125
|
+
@resource_request.action = @resource_request.id.blank? ? :index : :show
|
126
|
+
elsif @resource_request.request.put?
|
127
|
+
@resource_request.action = @resource_request.id.blank? ? :replace : :update
|
128
|
+
elsif @resource_request.request.post?
|
129
|
+
@resource_request.action = @resource_request.id.blank? ? :create : :add
|
130
|
+
elsif @resource_request.request.delete?
|
131
|
+
@resource_request.action = @resource_request.id.blank? ? :drop : :destroy
|
132
|
+
else
|
133
|
+
raise HTTP405MethodNotAllowed, 'Action not provided or found and unknown HTTP request method.'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.keyed_with_type(klass)
|
139
|
+
# Allows decendent controllers to set a data type for the id other than the default.
|
140
|
+
@@key_type = klass
|
141
|
+
end
|
142
|
+
|
143
|
+
def format_id
|
144
|
+
# This method is used to convert the id coming off of the path stack, which is in string form, into another data type if one has been set.
|
145
|
+
@@key_type ||= nil
|
146
|
+
unless @@key_type.blank?
|
147
|
+
if @@key_type == Fixnum
|
148
|
+
@resource_request.id = @resource_request.id.to_i
|
149
|
+
elsif @@key_type == Float
|
150
|
+
@resource_request.id = @resource_request.id.to_f
|
151
|
+
else
|
152
|
+
raise HTTP500ServerError, "Invalid key identifier type specified on resource #{self.class.to_s}."
|
153
|
+
end
|
154
|
+
else
|
155
|
+
@@key_type = String
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def package(data)
|
160
|
+
# This handles outputing properly formatted content based on the file extension in the URL.
|
161
|
+
if @resource_request.mime_type.like?( RESTRack.mime_type_for( :JSON ) )
|
162
|
+
@output = data.to_json
|
163
|
+
elsif @resource_request.mime_type.like?( RESTRack.mime_type_for( :XML ) )
|
164
|
+
if File.exists? builder_file
|
165
|
+
@output = builder_up(data)
|
166
|
+
else
|
167
|
+
@output = XmlSimple.xml_out(data, 'AttrPrefix' => true)
|
168
|
+
end
|
169
|
+
elsif @resource_request.mime_type.like?(RESTRack.mime_type_for( :YAML ) )
|
170
|
+
@output = YAML.dump(data)
|
171
|
+
elsif @resource_request.mime_type.like?(RESTRack.mime_type_for( :TEXT ) )
|
172
|
+
@output = data.to_s
|
173
|
+
else
|
174
|
+
@output = data
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def builder_up(data)
|
179
|
+
# Use Builder to generate the XML
|
180
|
+
buffer = ''
|
181
|
+
xml = Builder::XmlMarkup.new(:target => buffer)
|
182
|
+
xml.instruct!
|
183
|
+
eval( File.new( builder_file ).read )
|
184
|
+
return buffer
|
185
|
+
end
|
186
|
+
|
187
|
+
def builder_file
|
188
|
+
"#{RESTRack::CONFIG[:ROOT]}/views/#{@resource_request.resource_name.underscore}/#{@resource_request.action}.xml.builder"
|
189
|
+
end
|
190
|
+
|
191
|
+
end # class ResourceController
|
192
|
+
end # module RESTRack
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module RESTRack
|
2
|
+
class ResourceRequest
|
3
|
+
attr_reader :request, :request_id, :input
|
4
|
+
attr_accessor :mime_type, :path_stack, :resource_name, :action, :id
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
# Initialize the ResourceRequest by assigning a request_id and determining the path, format, and controller of the resource.
|
8
|
+
# Accepting options just to allow us to override request_id for testing.
|
9
|
+
@request = opts[:request]
|
10
|
+
@request_id = opts[:request_id] || get_request_id
|
11
|
+
|
12
|
+
# Write input details to logs
|
13
|
+
RESTRack.request_log.info "{#{@request_id}} #{@request.path_info} requested from #{@request.ip}"
|
14
|
+
|
15
|
+
RESTRack.log.debug "{#{@request_id}} Reading POST Input"
|
16
|
+
|
17
|
+
# Pull input data from POST body
|
18
|
+
@input = read( @request )
|
19
|
+
|
20
|
+
# Setup up the initial routing.
|
21
|
+
(@path_stack, extension) = split_extension_from( @request.path_info )
|
22
|
+
@mime_type = get_mime_type_from( extension )
|
23
|
+
(@resource_name, @path_stack) = get_initial_resource_from( @path_stack )
|
24
|
+
(@id, @action, @path_stack) = get_id_and_action_from( @path_stack )
|
25
|
+
|
26
|
+
# Verify that initial resource in the request chain is accessible at the root.
|
27
|
+
raise HTTP403Forbidden unless RESTRack::CONFIG[:ROOT_RESOURCE_ACCEPT].blank? or RESTRack::CONFIG[:ROOT_RESOURCE_ACCEPT].include?(@resource_name)
|
28
|
+
raise HTTP403Forbidden if not RESTRack::CONFIG[:ROOT_RESOURCE_DENY].blank? and RESTRack::CONFIG[:ROOT_RESOURCE_DENY].include?(@resource_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def locate
|
32
|
+
# Locate the correct controller of resource based on the request.
|
33
|
+
# The resource requested must be a member of RESTRack application or a 404 error will be thrown by RESTRack::WebService.
|
34
|
+
RESTRack.log.debug "{#{@request_id}} Locating Resource"
|
35
|
+
@resource = instantiate_controller
|
36
|
+
end
|
37
|
+
|
38
|
+
def call
|
39
|
+
# Pass along the `call` method to the typed resource object, this must occur after a call to locate.
|
40
|
+
RESTRack.log.debug "{#{@request_id}} Processing Request"
|
41
|
+
@resource.call
|
42
|
+
end
|
43
|
+
|
44
|
+
def output
|
45
|
+
# Send out the typed resource's output, this must occur after a call to run.
|
46
|
+
RESTRack.log.debug "{#{@request_id}} Retrieving Output"
|
47
|
+
@resource.output
|
48
|
+
end
|
49
|
+
|
50
|
+
def content_type
|
51
|
+
@mime_type.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def get_request_id
|
56
|
+
t = Time.now
|
57
|
+
return t.strftime('%FT%T') + '.' + t.usec.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def read(request)
|
61
|
+
input = ''
|
62
|
+
unless request.content_type.blank?
|
63
|
+
request_mime_type = MIME::Type.new( request.content_type )
|
64
|
+
if request_mime_type.like?( RESTRack.mime_type_for( :JSON ) )
|
65
|
+
input = JSON.parse( request.body.read )
|
66
|
+
elsif request_mime_type.like?( RESTRack.mime_type_for( :XML ) )
|
67
|
+
input = XmlSimple.xml_out( request.body.read )
|
68
|
+
elsif request_mime_type.like?( RESTRack.mime_type_for( :YAML ) )
|
69
|
+
input = YAML.parse( request.body.read )
|
70
|
+
elsif request_mime_type.like?( RESTRack.mime_type_for( :TEXT ) )
|
71
|
+
input = request.body.read.to_s
|
72
|
+
else
|
73
|
+
input = request.body.read
|
74
|
+
end
|
75
|
+
RESTRack.request_log.debug "{#{@request_id}} #{request_mime_type.to_s} data in\n" + input.to_json
|
76
|
+
end
|
77
|
+
input
|
78
|
+
end
|
79
|
+
|
80
|
+
def split_extension_from(path_stack)
|
81
|
+
# Remove the extension from the URL if present, that will be used to determine content-type.
|
82
|
+
extension = ''
|
83
|
+
unless path_stack.nil?
|
84
|
+
path_stack = path_stack.sub(/\.([^.]*)$/) do |s|
|
85
|
+
extension = $1.downcase
|
86
|
+
'' # Return an empty string as the substitution so that the extension is removed from `path_stack`
|
87
|
+
end
|
88
|
+
end
|
89
|
+
[path_stack, extension]
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_mime_type_from(extension)
|
93
|
+
unless extension == ''
|
94
|
+
mime_type = RESTRack.mime_type_for( extension )
|
95
|
+
end
|
96
|
+
if mime_type.nil?
|
97
|
+
if RESTRack::CONFIG[:DEFAULT_FORMAT]
|
98
|
+
mime_type = RESTRack.mime_type_for( RESTRack::CONFIG[:DEFAULT_FORMAT].to_s.downcase )
|
99
|
+
else
|
100
|
+
mime_type = RESTRack.mime_type_for( :JSON )
|
101
|
+
end
|
102
|
+
end
|
103
|
+
mime_type
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_initial_resource_from(orig_path_stack)
|
107
|
+
( empty_str, resource_name, path_stack ) = orig_path_stack.split('/', 3)
|
108
|
+
if resource_name.blank? or not RESTRack.resource_exists? resource_name # treat as if request to default resource
|
109
|
+
raise HTTP404ResourceNotFound if RESTRack::CONFIG[:DEFAULT_RESOURCE].blank?
|
110
|
+
path_stack = orig_path_stack.sub(/^\//, '')
|
111
|
+
resource_name = RESTRack::CONFIG[:DEFAULT_RESOURCE]
|
112
|
+
end
|
113
|
+
[resource_name, path_stack]
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_id_and_action_from(path_stack)
|
117
|
+
( id, action, path_stack ) = (path_stack || '').split('/', 3)
|
118
|
+
if [ :index, :replace, :create, :destroy ].include? id
|
119
|
+
action = id
|
120
|
+
id = nil
|
121
|
+
end
|
122
|
+
[id, action, path_stack]
|
123
|
+
end
|
124
|
+
|
125
|
+
def instantiate_controller
|
126
|
+
# Called from the locate method, this method dynamically finds the class based on the URI and instantiates an object of that class via the __init method on RESTRack::ResourceController.
|
127
|
+
begin
|
128
|
+
return RESTRack.controller_class_for( @resource_name ).__init(self)
|
129
|
+
rescue
|
130
|
+
raise HTTP404ResourceNotFound, "The resource #{RESTRack::CONFIG[:SERVICE_NAME]}::#{RESTRack.controller_name(@resource_name)} could not be instantiated."
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end # class ResourceRequest
|
135
|
+
end # module RESTRack
|