restrack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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