toast 0.9.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,207 @@
1
+ require 'toast/request_helpers'
2
+ require 'link_header'
3
+
4
+ class Toast::SingularAssocRequest
5
+ include Toast::RequestHelpers
6
+ include Toast::Errors
7
+
8
+ def initialize id, config, base_config, auth, request
9
+ @id = id
10
+ @config = config
11
+ @base_config = base_config
12
+ @selected_attributes = request.query_parameters.delete(:toast_select).try(:split,',')
13
+ @uri_params = request.query_parameters
14
+ @base_uri = base_uri(request)
15
+ @verb = request.request_method.downcase
16
+ @auth = auth
17
+ @request = request
18
+ end
19
+
20
+ def respond
21
+ if @verb.in? %w(get link unlink)
22
+ self.send(@verb)
23
+ else
24
+ response :method_not_allowed,
25
+ headers: {'Allow' => allowed_methods(@config)},
26
+ msg: "#{@verb.upcase} not supported for singular association requests"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def get
33
+ if @config.via_get.nil?
34
+ response :method_not_allowed,
35
+ headers: {'Allow' => allowed_methods(@config)},
36
+ msg: "GET not configured"
37
+ else
38
+ begin
39
+ source = @base_config.model_class.find(@id)
40
+ target_config = get_config(@config.target_model_class)
41
+ model_instance = @config.via_get.handler.call(source, @uri_params)
42
+ call_allow(@config.via_get.permissions, @auth, source, @uri_params)
43
+
44
+ unless model_instance.is_a? @config.target_model_class
45
+ # wrong class
46
+ response :internal_server_error,
47
+ msg: "singular association returned `#{model_instance.class}', expected `#{@config.target_model_class}'"
48
+ else
49
+
50
+ response :ok,
51
+ headers: {"Content-Type" => @config.media_type},
52
+ body: represent(model_instance, target_config),
53
+ msg: "sent ##{@config.model_class}##{@id}"
54
+ end
55
+
56
+ rescue NotAllowed => error
57
+ return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
58
+
59
+
60
+ rescue BadRequest => error
61
+ response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
62
+
63
+ rescue HandlerError => error
64
+ return response :internal_server_error,
65
+ msg: "exception raised in via_get handler: `#{error.orig_error.message}' in #{error.source_location}"
66
+
67
+ rescue ActiveRecord::RecordNotFound
68
+ response :not_found,
69
+ msg: "#{@config.model_class.name}##{@config.assoc_name} not found"
70
+
71
+ rescue ConfigNotFound => error
72
+ response :internal_server_error,
73
+ msg: "no API configuration found for model `#{@config.target_model_class.name}'"
74
+
75
+ rescue => error
76
+ response :internal_server_error,
77
+ msg: "exception from via_get handler: #{error.message}"
78
+ end
79
+ end
80
+ end
81
+
82
+ def link
83
+ if @config.via_link.nil?
84
+ response :method_not_allowed,
85
+ headers: {'Allow' => allowed_methods(@config)},
86
+ msg: "LINK not configured"
87
+ else
88
+
89
+ begin
90
+ source = @base_config.model_class.find(@id)
91
+ begin
92
+
93
+ call_allow(@config.via_link.permissions, @auth, source, @uri_params)
94
+
95
+
96
+ link = LinkHeader.parse(@request.headers['Link']).find_link(['ref','related'])
97
+
98
+ if link.nil?
99
+ return response :bad_request, msg: "Link header missing or invalid"
100
+ end
101
+
102
+ name, target_id = URI(link.href).path.split('/')[1..-1]
103
+ target_model_class = name.singularize.classify.constantize
104
+
105
+ unless is_active_record? target_model_class
106
+ return response :not_found, msg: "target class `#{target_model_class.name}' is not an `ActiveRecord'"
107
+ end
108
+
109
+ if target_model_class != @config.target_model_class
110
+ return response :bad_request,
111
+ msg: "target class `#{target_model_class.name}' invalid, expect: `#{@config.target_model_class}'"
112
+ end
113
+
114
+ @config.via_link.handler.call(source, target_model_class.find(target_id), @uri_params)
115
+ response :ok,
116
+ msg: "linked #{source.class}##{source.id} with #{target_model_class.name}##{@id}"
117
+
118
+ rescue NotAllowed => error
119
+ return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
120
+
121
+ rescue BadRequest => error
122
+ response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
123
+
124
+ rescue HandlerError => error
125
+ return response :internal_server_error,
126
+ msg: "exception raised in via_link handler: `#{error.orig_error.message}' in #{error.source_location}"
127
+
128
+ rescue ActiveRecord::RecordNotFound # target not found
129
+ response :not_found, msg: "#{target_model_class.name}##{target_id} not found"
130
+
131
+ rescue => error
132
+ response :internal_server_error, msg: "exception from via_link handler: #{error.message}"
133
+ end
134
+ rescue ActiveRecord::RecordNotFound # source not found
135
+ response :not_found, msg: "#{@base_config.model_class.name}##{@id} not found"
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ def unlink
142
+ if @config.via_unlink.nil?
143
+ response :method_not_allowed,
144
+ headers: {'Allow' => allowed_methods(@config)},
145
+ msg: "UNLINK not configured"
146
+ else
147
+ begin
148
+ source = @base_config.model_class.find(@id)
149
+ call_allow(@config.via_unlink.permissions, @auth, source, @uri_params)
150
+
151
+ link = LinkHeader.parse(@request.headers['Link']).find_link(['ref','related'])
152
+
153
+ if link.nil?
154
+ return response :bad_request, msg: "Link header missing or invalid"
155
+ end
156
+
157
+ name, target_id = URI(link.href).path.split('/')[1..-1]
158
+ target_model_class = name.singularize.classify.constantize
159
+
160
+ unless is_active_record? target_model_class
161
+ return response :not_found, msg: "target class `#{target_model_class.name}' is not an `ActiveRecord'"
162
+ end
163
+
164
+ if target_model_class != @config.target_model_class
165
+ return response :bad_request,
166
+ msg: "target class `#{target_model_class.name}' invalid, expect: `#{@config.target_model_class}'"
167
+ end
168
+
169
+ target = nil
170
+ begin
171
+ target = target_model_class.find(target_id)
172
+ rescue ActiveRecord::RecordNotFound # target
173
+ return response :not_found, msg: "#{target_model_class.name}##{target_id} not found"
174
+ end
175
+
176
+ current = source.send(@config.assoc_name)
177
+
178
+ if current != target
179
+ return response :conflict, msg: "target `#{current.class}##{current.id}' is not associated, cannot unlink `#{target.class}##{target.id}'"
180
+ end
181
+
182
+ call_handler(@config.via_unlink.handler, source, target, @uri_params)
183
+
184
+ response :ok,
185
+ msg: "unlinked #{source.class}##{source.id} from #{target_model_class.name}##{target_id}"
186
+
187
+ rescue NotAllowed => error
188
+ return response :unauthorized,
189
+ msg: "not authorized by allow block in: #{error.source_location}"
190
+
191
+ rescue BadRequest => error
192
+ response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
193
+
194
+ rescue HandlerError => error
195
+ return response :internal_server_error,
196
+ msg: "exception raised in via_unlink handler: `#{error.orig_error.message}' in #{error.source_location}"
197
+
198
+ rescue AllowError => error
199
+ return response :internal_server_error,
200
+ msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
201
+
202
+ rescue ActiveRecord::RecordNotFound # source
203
+ return response :not_found, msg: "#{@base_config.model_class.name}##{@id} not found"
204
+ end
205
+ end
206
+ end
207
+ end
data/lib/toast/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Toast
2
- VERSION = '0.9.5'
3
- end
2
+ VERSION = '1.0.0'
3
+ end
data/lib/toast.rb CHANGED
@@ -1 +1,100 @@
1
- require 'toast/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
1
+ unless defined?(Rails)
2
+ puts 'The Toast gem is made for Ruby on Rails 5.'
3
+ exit 1
4
+ end
5
+
6
+ unless Rails::VERSION::MAJOR == 5
7
+ puts 'Toast v1 requires Ruby on Rails 5, try v0.9 for older rails.'
8
+ exit 1
9
+ end
10
+
11
+ require 'ostruct'
12
+ require 'toast/engine'
13
+ require 'toast/config_dsl'
14
+ require 'toast/rack_app'
15
+
16
+ module Toast
17
+ Sym = "\xF0\x9F\x8D\x9E" # The BREAD
18
+
19
+ # config data of all expose blocks
20
+ @@expositions = []
21
+
22
+ # collects all configs of one expose block (DSL methods write to it)
23
+ @@current_expose = nil
24
+
25
+ cattr_accessor :expositions, :settings
26
+
27
+ class ConfigError < StandardError
28
+ end
29
+
30
+ # called once on boot via enigne.rb
31
+ def self.init config_path='config/toast-api/*', settings_path='config/toast-api.rb'
32
+
33
+ # clean up
34
+ Toast.expositions.clear
35
+ Toast.settings = nil
36
+
37
+ settings = ''
38
+ # read global settings
39
+ if File.exists? settings_path
40
+ open settings_path do |f|
41
+ settings = f.read
42
+ end
43
+ else
44
+ info "No global settings file found: `#{settings_path}', using defaults"
45
+ settings_path = '[defaults]'
46
+ end
47
+
48
+ Toast::ConfigDSL.get_settings settings, settings_path
49
+
50
+ # read configurations
51
+ config_files = Dir[config_path]
52
+
53
+ if config_files.empty?
54
+ Toast.raise_config_error "No config files found in `#{config_path}`"
55
+ else
56
+
57
+ config_files.each do |fname|
58
+ open fname do |f|
59
+ config = f.read
60
+ Toast::ConfigDSL.get_config(config, fname)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.info str
67
+ if Rails.const_defined?('Server') # only on console server
68
+ puts Toast::Sym+' Toast: '+str
69
+ end
70
+ end
71
+
72
+ def self.disable message=''
73
+ info "Disabeling resource exposition due to config errors."
74
+ info message unless message.blank?
75
+
76
+ @@expositions.clear
77
+ end
78
+
79
+ def self.raise_config_error message
80
+ raise ConfigError.new("CONFIG ERROR: #{message}")
81
+ end
82
+
83
+ def self.logger
84
+ @@logger ||= Logger.new("#{Rails.root}/log/toast.log")
85
+ end
86
+
87
+ # get the representation (as Hash) by instance (w/o request)
88
+ # base_uri must be passed to be prepended in URIs
89
+ def self.represent instance, base_uri = ''
90
+
91
+ # using RequestHelper#represent_one method with a mocked up object
92
+ obj = Object.new
93
+ class << obj
94
+ include Toast::RequestHelpers
95
+ attr_accessor :base_uri
96
+ end
97
+ obj.base_uri = base_uri
98
+ obj.represent_one(instance, obj.get_config(instance.class) )
99
+ end
100
+ end
metadata CHANGED
@@ -1,149 +1,116 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: toast
3
- version: !ruby/object:Gem::Version
4
- hash: 49
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 9
9
- - 5
10
- version: 0.9.5
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - robokopp (Robert Annies)
14
8
  autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
-
18
- date: 2018-07-19 00:00:00 +00:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
11
+ date: 2017-09-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
22
14
  name: rails
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- hash: 3
30
- segments:
31
- - 3
32
- - 1
33
- - 0
34
- version: 3.1.0
35
- - - <
36
- - !ruby/object:Gem::Version
37
- hash: 63
38
- segments:
39
- - 4
40
- - 0
41
- - 0
42
- version: 4.0.0
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
43
20
  type: :runtime
44
- version_requirements: *id001
45
- - !ruby/object:Gem::Dependency
46
- name: blockenspiel
47
21
  prerelease: false
48
- requirement: &id002 !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
51
24
  - - ~>
52
- - !ruby/object:Gem::Version
53
- hash: 11
54
- segments:
55
- - 0
56
- - 4
57
- - 2
58
- version: 0.4.2
59
- type: :runtime
60
- version_requirements: *id002
61
- - !ruby/object:Gem::Dependency
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
62
28
  name: rack-accept-media-types
63
- prerelease: false
64
- requirement: &id003 !ruby/object:Gem::Requirement
65
- none: false
66
- requirements:
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
67
31
  - - ~>
68
- - !ruby/object:Gem::Version
69
- hash: 25
70
- segments:
71
- - 0
72
- - 9
73
- version: "0.9"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
74
34
  type: :runtime
75
- version_requirements: *id003
76
- - !ruby/object:Gem::Dependency
77
- name: rack-link_headers
78
35
  prerelease: false
79
- requirement: &id004 !ruby/object:Gem::Requirement
80
- none: false
81
- requirements:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: link_header
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
82
45
  - - ~>
83
- - !ruby/object:Gem::Version
84
- hash: 3
85
- segments:
86
- - 2
87
- - 2
88
- - 2
89
- version: 2.2.2
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.8
90
48
  type: :runtime
91
- version_requirements: *id004
92
- description: Toast is an extension to Ruby on Rails 3 that lets you expose any ActiveRecord model as a web resource. Operations follow the REST/Hypermedia API principles implemented by a generic hidden controller.
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.8
55
+ description: "Toast is a Rack application that hooks into Ruby on Rails. It exposes
56
+ ActiveRecord models as \na web service (REST API). The main difference from doing
57
+ that with Ruby on Rails itself is \nit's DSL that covers all aspects of an API in
58
+ one single configuration.\n"
93
59
  email: robokopp@fernwerk.net
94
60
  executables: []
95
-
96
61
  extensions: []
97
-
98
- extra_rdoc_files:
62
+ extra_rdoc_files:
63
+ - README.md
64
+ files:
99
65
  - README.md
100
- files:
101
- - app/controller/toast_controller.rb
102
66
  - config/routes.rb
67
+ - lib/generators/toast/USAGE
68
+ - lib/generators/toast/templates/toast-api.rb.erb
69
+ - lib/generators/toast/toast_generator.rb
103
70
  - lib/toast.rb
104
- - lib/toast/version.rb
105
- - lib/toast/active_record_extensions.rb
106
- - lib/toast/association.rb
107
- - lib/toast/collection.rb
71
+ - lib/toast/canonical_request.rb
72
+ - lib/toast/collection_request.rb
108
73
  - lib/toast/config_dsl.rb
74
+ - lib/toast/config_dsl/association.rb
75
+ - lib/toast/config_dsl/base.rb
76
+ - lib/toast/config_dsl/collection.rb
77
+ - lib/toast/config_dsl/common.rb
78
+ - lib/toast/config_dsl/default_handlers.rb
79
+ - lib/toast/config_dsl/expose.rb
80
+ - lib/toast/config_dsl/settings.rb
81
+ - lib/toast/config_dsl/single.rb
82
+ - lib/toast/config_dsl/via_verb.rb
109
83
  - lib/toast/engine.rb
110
- - lib/toast/record.rb
111
- - lib/toast/resource.rb
112
- - lib/toast/single.rb
113
- - README.md
114
- has_rdoc: true
84
+ - lib/toast/errors.rb
85
+ - lib/toast/http_range.rb
86
+ - lib/toast/plural_assoc_request.rb
87
+ - lib/toast/rack_app.rb
88
+ - lib/toast/request_helpers.rb
89
+ - lib/toast/single_request.rb
90
+ - lib/toast/singular_assoc_request.rb
91
+ - lib/toast/version.rb
115
92
  homepage: https://github.com/robokopp/toast
116
93
  licenses: []
117
-
94
+ metadata: {}
118
95
  post_install_message:
119
96
  rdoc_options: []
120
-
121
- require_paths:
97
+ require_paths:
122
98
  - lib
123
- required_ruby_version: !ruby/object:Gem::Requirement
124
- none: false
125
- requirements:
126
- - - ">="
127
- - !ruby/object:Gem::Version
128
- hash: 3
129
- segments:
130
- - 0
131
- version: "0"
132
- required_rubygems_version: !ruby/object:Gem::Requirement
133
- none: false
134
- requirements:
135
- - - ">="
136
- - !ruby/object:Gem::Version
137
- hash: 3
138
- segments:
139
- - 0
140
- version: "0"
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
141
109
  requirements: []
142
-
143
110
  rubyforge_project:
144
- rubygems_version: 1.3.7
111
+ rubygems_version: 2.4.6
145
112
  signing_key:
146
- specification_version: 3
147
- summary: Toast adds a Hypermedia API to ActiveRecord models in Ruby on Rails.
113
+ specification_version: 4
114
+ summary: Toast exposes ActiveRecord models as a web service (REST API).
148
115
  test_files: []
149
-
116
+ has_rdoc:
@@ -1,103 +0,0 @@
1
- require 'rack-link_headers'
2
-
3
- class ToastController < ApplicationController
4
-
5
- def catch_all
6
-
7
- begin
8
-
9
- @resource = Toast::Resource.build( params, request )
10
-
11
- unless request.headers["LINK"].nil?
12
- # extract "path_info" from link header
13
- port = if(request.port != 80 and request.port != 443)
14
- ":#{request.port}"
15
- else
16
- ""
17
- end
18
-
19
- request.headers["LINK"] =~ /(#{request.protocol + request.host + port + request.script_name})(.*)/
20
- end
21
-
22
- toast_response = @resource.apply(request.method, request.body.read, request.content_type, $2)
23
-
24
- # pagination
25
- if pi = toast_response[:pagination_info]
26
- # URL w/o parameters
27
-
28
- url = request.url.split('?').first
29
- qpar = request.query_parameters.clone
30
-
31
- # change/add page parameter
32
- link_header = []
33
-
34
- if pi[:prev]
35
- qpar[:page] = pi[:prev]
36
- response.link "#{url}?#{qpar.to_query}", :rel => :prev
37
- end
38
-
39
- if pi[:next]
40
- qpar[:page] = pi[:next]
41
- response.link "#{url}?#{qpar.to_query}", :rel => :next
42
- end
43
-
44
- qpar[:page] = pi[:last]
45
- response.link "#{url}?#{qpar.to_query}", :rel => :last
46
-
47
- qpar[:page] = 1
48
- response.link "#{url}?#{qpar.to_query}", :rel => :first
49
-
50
- response.headers["X-Collection-Size"] = pi[:total].to_s
51
-
52
- end
53
-
54
- render toast_response
55
-
56
- rescue Toast::ResourceNotFound => e
57
- return head(:not_found)
58
-
59
- rescue Toast::PayloadInvalid => e
60
- return render :text => e.message, :status => :forbidden
61
-
62
- rescue Toast::PayloadFormatError => e
63
- return head(:bad_request)
64
-
65
- rescue Toast::BadRequest => e
66
- return head(:bad_request)
67
-
68
- rescue Toast::MethodNotAllowed => e
69
- return head(:method_not_allowed)
70
-
71
- rescue Toast::UnsupportedMediaType => e
72
- return head(:unsupported_media_type)
73
-
74
- rescue Toast::ResourceNotAcceptable => e
75
- return head(:not_acceptable)
76
-
77
- rescue Toast::Conflict => e
78
- return render :text => e.message, :status => :conflict
79
-
80
- rescue Exception => e
81
- log_exception e
82
- puts e if Rails.env == "test"
83
- return head(:internal_server_error)
84
- end
85
-
86
- end
87
-
88
- def not_found
89
- return head(:not_found)
90
- end
91
-
92
-
93
- if Rails.env == "test"
94
- def log_exception e
95
- puts "#{e.class}: '#{e.message}'\n\n#{e.backtrace[0..14].join("\n")}\n\n"
96
- end
97
- else
98
- def log_exception e
99
- logger.error("#{e.class}: '#{e.message}'\n\n#{e.backtrace.join("\n")}")
100
- end
101
- end
102
-
103
- end