toast 0.9.5 → 1.0.0

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.
@@ -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