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.
Files changed (60) hide show
  1. data/Gemfile +4 -0
  2. data/README.rdoc +71 -0
  3. data/Rakefile +32 -0
  4. data/bin/restrack +23 -0
  5. data/config/constants.yaml +8 -0
  6. data/lib/restrack/generator/constants.yaml.erb +24 -0
  7. data/lib/restrack/generator/controller.rb.erb +35 -0
  8. data/lib/restrack/generator/loader.rb.erb +21 -0
  9. data/lib/restrack/generator.rb +93 -0
  10. data/lib/restrack/http_status.rb +10 -0
  11. data/lib/restrack/resource_controller.rb +192 -0
  12. data/lib/restrack/resource_request.rb +135 -0
  13. data/lib/restrack/support.rb +56 -0
  14. data/lib/restrack/version.rb +3 -0
  15. data/lib/restrack/web_service.rb +66 -0
  16. data/lib/restrack.rb +24 -0
  17. data/restrack.gemspec +28 -0
  18. data/test/sample_app_1/config/constants.yaml +25 -0
  19. data/test/sample_app_1/controllers/bat_controller.rb +9 -0
  20. data/test/sample_app_1/controllers/baz_controller.rb +9 -0
  21. data/test/sample_app_1/controllers/baza_controller.rb +11 -0
  22. data/test/sample_app_1/controllers/bazu_controller.rb +16 -0
  23. data/test/sample_app_1/controllers/foo_bar_controller.rb +62 -0
  24. data/test/sample_app_1/loader.rb +31 -0
  25. data/test/sample_app_1/test/test_controller_actions.rb +122 -0
  26. data/test/sample_app_1/test/test_controller_modifiers.rb +153 -0
  27. data/test/sample_app_1/test/test_formats.rb +119 -0
  28. data/test/sample_app_1/test/test_resource_request.rb +160 -0
  29. data/test/sample_app_1/test/test_web_service.rb +27 -0
  30. data/test/sample_app_1/views/foo_bar/show.xml.builder +4 -0
  31. data/test/sample_app_2/config/constants.yaml +24 -0
  32. data/test/sample_app_2/controllers/bat_controller.rb +9 -0
  33. data/test/sample_app_2/controllers/baz_controller.rb +9 -0
  34. data/test/sample_app_2/controllers/baza_controller.rb +11 -0
  35. data/test/sample_app_2/controllers/bazu_controller.rb +8 -0
  36. data/test/sample_app_2/controllers/foo_bar_controller.rb +59 -0
  37. data/test/sample_app_2/loader.rb +31 -0
  38. data/test/sample_app_2/test/test_controller_modifiers.rb +121 -0
  39. data/test/sample_app_2/test/test_resource_request.rb +71 -0
  40. data/test/sample_app_2/views/foo_bar/show.xml.builder +4 -0
  41. data/test/sample_app_3/config/constants.yaml +24 -0
  42. data/test/sample_app_3/controllers/bat_controller.rb +9 -0
  43. data/test/sample_app_3/controllers/baz_controller.rb +9 -0
  44. data/test/sample_app_3/controllers/baza_controller.rb +11 -0
  45. data/test/sample_app_3/controllers/bazu_controller.rb +8 -0
  46. data/test/sample_app_3/controllers/foo_bar_controller.rb +59 -0
  47. data/test/sample_app_3/loader.rb +31 -0
  48. data/test/sample_app_3/test/test_resource_request.rb +42 -0
  49. data/test/sample_app_3/views/foo_bar/show.xml.builder +4 -0
  50. data/test/sample_app_4/config/constants.yaml +24 -0
  51. data/test/sample_app_4/controllers/bar_controller.rb +11 -0
  52. data/test/sample_app_4/controllers/baz_controller.rb +15 -0
  53. data/test/sample_app_4/controllers/foo_controller.rb +21 -0
  54. data/test/sample_app_4/loader.rb +31 -0
  55. data/test/sample_app_4/test/test_controller_modifiers.rb +28 -0
  56. data/test/sample_app_4/test/test_formats.rb +49 -0
  57. data/test/sample_app_4/views/alphatest.png +0 -0
  58. data/test/test_support.rb +20 -0
  59. data/test/test_web_service.rb +31 -0
  60. metadata +238 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specifying gem's dependencies in restrack.gemspec
4
+ gemspec
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