restrack 0.0.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/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
|