shaf 0.8.0 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/bin/shaf +19 -4
- data/lib/shaf.rb +1 -0
- data/lib/shaf/app.rb +7 -10
- data/lib/shaf/command/server.rb +4 -2
- data/lib/shaf/errors.rb +22 -2
- data/lib/shaf/extensions/controller_hooks.rb +14 -0
- data/lib/shaf/extensions/resource_uris.rb +1 -1
- data/lib/shaf/formable/field.rb +10 -6
- data/lib/shaf/formable/form.rb +4 -0
- data/lib/shaf/generator/base.rb +2 -2
- data/lib/shaf/generator/controller.rb +11 -11
- data/lib/shaf/generator/helper.rb +33 -0
- data/lib/shaf/generator/migration.rb +6 -1
- data/lib/shaf/generator/migration/empty.rb +2 -2
- data/lib/shaf/generator/serializer.rb +4 -4
- data/lib/shaf/generator/templates/api/controller.rb.erb +4 -3
- data/lib/shaf/generator/templates/api/serializer.rb.erb +10 -12
- data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +4 -2
- data/lib/shaf/helpers.rb +2 -0
- data/lib/shaf/helpers/http_header.rb +26 -0
- data/lib/shaf/helpers/paginate.rb +4 -2
- data/lib/shaf/helpers/payload.rb +33 -8
- data/lib/shaf/middleware/request_id.rb +3 -1
- data/lib/shaf/rake/db.rb +5 -5
- data/lib/shaf/rake/test.rb +0 -27
- data/lib/shaf/router.rb +133 -0
- data/lib/shaf/settings.rb +36 -11
- data/lib/shaf/spec/http_method_utils.rb +11 -3
- data/lib/shaf/spec/integration_spec.rb +4 -4
- data/lib/shaf/spec/serializer_spec.rb +1 -1
- data/lib/shaf/upgrade/manifest.rb +34 -9
- data/lib/shaf/upgrade/package.rb +24 -15
- data/lib/shaf/utils.rb +31 -21
- data/lib/shaf/version.rb +1 -1
- data/templates/Rakefile +27 -0
- data/templates/api/controllers/base_controller.rb +13 -8
- data/templates/api/serializers/error_serializer.rb +3 -1
- data/templates/api/serializers/form_serializer.rb +11 -16
- data/templates/api/serializers/validation_error_serializer.rb +13 -0
- data/templates/config.ru +1 -1
- data/templates/config/bootstrap.rb +3 -1
- data/templates/config/database.rb +6 -7
- data/templates/config/directories.rb +3 -2
- data/templates/config/helpers.rb +1 -1
- data/templates/config/initializers/db_migrations.rb +14 -8
- data/templates/config/initializers/logging.rb +2 -2
- data/templates/config/initializers/sequel.rb +1 -0
- data/templates/config/paths.rb +7 -0
- data/templates/config/settings.yml +5 -0
- data/upgrades/1.0.0.tar.gz +0 -0
- metadata +38 -19
- metadata.gz.sig +0 -0
- data/templates/config/constants.rb +0 -8
@@ -5,16 +5,14 @@ class <%= class_name %> < BaseSerializer
|
|
5
5
|
|
6
6
|
model <%= model_class_name %>
|
7
7
|
policy <%= policy_class_name %>
|
8
|
-
|
9
|
-
<%=
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
<%= collection_with_doc.join("\n ") %>
|
19
|
-
<% end %>
|
8
|
+
|
9
|
+
<%= print_nested(attributes_with_doc) %>
|
10
|
+
|
11
|
+
<%= print_nested(curies_with_doc) %>
|
12
|
+
|
13
|
+
<%= print_nested(links_with_doc) %>
|
14
|
+
|
15
|
+
<%- if collection_with_doc %>
|
16
|
+
<%= print(collection_with_doc) %>
|
17
|
+
<%- end -%>
|
20
18
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
2
3
|
|
3
4
|
describe <%= class_name %> do
|
4
5
|
|
@@ -6,10 +7,11 @@ describe <%= class_name %> do
|
|
6
7
|
<%= model_class_name %>.new.
|
7
8
|
tap { |<%= name %>| <%= name %>.id = 5 }
|
8
9
|
end
|
10
|
+
let(:user) { OpenStruct.new(id: 5, name: 'Bengt') }
|
9
11
|
|
10
12
|
describe "when current_user is nil" do
|
11
13
|
before do
|
12
|
-
|
14
|
+
serialize resource
|
13
15
|
end
|
14
16
|
|
15
17
|
it "serializes attributes" do
|
@@ -27,7 +29,7 @@ describe <%= class_name %> do
|
|
27
29
|
|
28
30
|
describe "when current_user is present" do
|
29
31
|
before do
|
30
|
-
|
32
|
+
serialize resource, current_user: user
|
31
33
|
end
|
32
34
|
|
33
35
|
it "serializes attributes" do
|
data/lib/shaf/helpers.rb
CHANGED
@@ -2,6 +2,7 @@ require 'shaf/helpers/cache_control'
|
|
2
2
|
require 'shaf/helpers/json_html'
|
3
3
|
require 'shaf/helpers/paginate'
|
4
4
|
require 'shaf/helpers/payload'
|
5
|
+
require 'shaf/helpers/http_header'
|
5
6
|
|
6
7
|
module Shaf
|
7
8
|
def self.helpers
|
@@ -10,6 +11,7 @@ module Shaf
|
|
10
11
|
JsonHtml,
|
11
12
|
Paginate,
|
12
13
|
Payload,
|
14
|
+
HttpHeader,
|
13
15
|
]
|
14
16
|
end
|
15
17
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'shaf/errors'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module HttpHeader
|
5
|
+
def request_headers
|
6
|
+
unless respond_to? :request
|
7
|
+
log.error <<~ERROR
|
8
|
+
|
9
|
+
|
10
|
+
Classes including the HttpHeader module must respond to #request
|
11
|
+
HttpHeader#request_headers called from #{self}.
|
12
|
+
ERROR
|
13
|
+
raise Errors::ServerError, 'Server bug'
|
14
|
+
end
|
15
|
+
|
16
|
+
request.env.each_with_object({}) do |(key, value), headers|
|
17
|
+
next unless key =~ /^HTTP_/
|
18
|
+
headers[key[5..-1].tr('_', '-')] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def request_header(header)
|
23
|
+
request_headers[header.to_s.upcase]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'shaf/settings'
|
2
|
+
|
1
3
|
module Shaf
|
2
4
|
module Paginate
|
3
5
|
|
@@ -6,7 +8,7 @@ module Shaf
|
|
6
8
|
page == 0 ? 1 : page
|
7
9
|
end
|
8
10
|
|
9
|
-
def paginate!(collection, per_page =
|
11
|
+
def paginate!(collection, per_page = Shaf::Settings.paginate_per_page)
|
10
12
|
unless collection.respond_to? :paginate
|
11
13
|
log.warn "Trying to paginate a collection that doesn't " \
|
12
14
|
"support pagination: #{collection}"
|
@@ -17,7 +19,7 @@ module Shaf
|
|
17
19
|
collection.paginate(current_page, per_page)
|
18
20
|
end
|
19
21
|
|
20
|
-
def paginate(collection, per_page =
|
22
|
+
def paginate(collection, per_page = Shaf::Settings.paginate_per_page)
|
21
23
|
paginate!(collection.dup, per_page)
|
22
24
|
end
|
23
25
|
end
|
data/lib/shaf/helpers/payload.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
1
5
|
module Shaf
|
2
6
|
module Payload
|
3
7
|
EXCLUDED_FORM_PARAMS = ['captures', 'splat'].freeze
|
@@ -48,22 +52,24 @@ module Shaf
|
|
48
52
|
return {} if input.empty?
|
49
53
|
|
50
54
|
if request.env['CONTENT_TYPE'] =~ %r(\Aapplication/(hal\+)?json)
|
51
|
-
JSON.parse(input)
|
55
|
+
JSON.parse(input, symbolize_names: true)
|
52
56
|
else
|
53
|
-
raise ::UnsupportedMediaTypeError.new(request: request)
|
57
|
+
raise Errors::UnsupportedMediaTypeError.new(request: request)
|
54
58
|
end
|
55
59
|
rescue StandardError
|
56
|
-
raise ::BadRequestError.new
|
60
|
+
raise Errors::BadRequestError.new
|
57
61
|
end
|
58
62
|
|
59
63
|
def safe_params(*fields)
|
60
64
|
return {} unless payload
|
61
|
-
|
62
|
-
|
65
|
+
|
66
|
+
fields = fields.map { |f| f.to_sym.downcase }.to_set
|
67
|
+
fields << :id
|
63
68
|
|
64
69
|
{}.tap do |allowed|
|
65
|
-
|
66
|
-
allowed[f
|
70
|
+
fields.each do |f|
|
71
|
+
allowed[f] = payload[f] if payload.key? f
|
72
|
+
allowed[f] ||= payload[f.to_s] if payload.key? f.to_s
|
67
73
|
end
|
68
74
|
end
|
69
75
|
end
|
@@ -72,6 +78,11 @@ module Shaf
|
|
72
78
|
return name == '_method'
|
73
79
|
end
|
74
80
|
|
81
|
+
def profile(value = nil)
|
82
|
+
return @profile unless value
|
83
|
+
@profile = value
|
84
|
+
end
|
85
|
+
|
75
86
|
def respond_with_collection(resource, status: 200, serializer: nil, **kwargs)
|
76
87
|
respond_with(
|
77
88
|
resource,
|
@@ -111,7 +122,7 @@ module Shaf
|
|
111
122
|
|
112
123
|
def respond_with_hal(resource, serialized)
|
113
124
|
log.debug "Response payload (#{resource.class}): #{serialized}"
|
114
|
-
content_type :hal
|
125
|
+
content_type :hal, content_type_params(resource)
|
115
126
|
body serialized
|
116
127
|
end
|
117
128
|
|
@@ -132,5 +143,19 @@ module Shaf
|
|
132
143
|
sha1 = Digest::SHA1.hexdigest payload
|
133
144
|
etag sha1, :weak # Weak or Strong??
|
134
145
|
end
|
146
|
+
|
147
|
+
def content_type_params(resource)
|
148
|
+
return {profile: profile} if profile
|
149
|
+
|
150
|
+
name =
|
151
|
+
case resource
|
152
|
+
when Formable::Form
|
153
|
+
Shaf::Settings.form_profile_name
|
154
|
+
when Errors::ServerError
|
155
|
+
Shaf::Settings.error_profile_name
|
156
|
+
end
|
157
|
+
|
158
|
+
{profile: name}.compact
|
159
|
+
end
|
135
160
|
end
|
136
161
|
end
|
data/lib/shaf/rake/db.rb
CHANGED
@@ -22,10 +22,10 @@ end
|
|
22
22
|
Shaf::DbTask.new(:migrate, description: "Run migrations", args: [:version]) do |t, args|
|
23
23
|
if args[:version]
|
24
24
|
puts "Migrating to version #{args[:version]}"
|
25
|
-
Sequel::Migrator.run(DB,
|
25
|
+
Sequel::Migrator.run(DB, Shaf::Settings.migrations_dir, target: args[:version].to_i)
|
26
26
|
else
|
27
27
|
puts "Migrating to latest"
|
28
|
-
Sequel::Migrator.run(DB,
|
28
|
+
Sequel::Migrator.run(DB, Shaf::Settings.migrations_dir)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -46,7 +46,7 @@ Shaf::DbTask.new(
|
|
46
46
|
puts "This would migrate the Database to version: #{version}. Continue [N/y]?"
|
47
47
|
next unless /\Ay/i =~ STDIN.gets.chomp&.downcase
|
48
48
|
end
|
49
|
-
Sequel::Migrator.run(DB,
|
49
|
+
Sequel::Migrator.run(DB, Shaf::Settings.migrations_dir, target: version.to_i)
|
50
50
|
end
|
51
51
|
|
52
52
|
Rake::Task["db:migrate"].enhance do
|
@@ -59,8 +59,8 @@ end
|
|
59
59
|
|
60
60
|
Shaf::DbTask.new(:reset, description: "Reset the database by deleting all rows in all columns") do
|
61
61
|
version = 0
|
62
|
-
Sequel::Migrator.run(DB,
|
63
|
-
Sequel::Migrator.run(DB,
|
62
|
+
Sequel::Migrator.run(DB, Shaf::Settings.migrations_dir, target: version)
|
63
|
+
Sequel::Migrator.run(DB, Shaf::Settings.migrations_dir)
|
64
64
|
end
|
65
65
|
|
66
66
|
Shaf::DbTask.new(:seed, description: "Seed the Database") do
|
data/lib/shaf/rake/test.rb
CHANGED
@@ -1,32 +1,5 @@
|
|
1
1
|
require 'shaf/tasks'
|
2
2
|
|
3
|
-
namespace :test do |ns|
|
4
|
-
Shaf::TestTask.new(:integration) do |t|
|
5
|
-
t.pattern = "spec/integration/**/*_spec.rb"
|
6
|
-
end
|
7
|
-
|
8
|
-
Shaf::TestTask.new(:models) do |t|
|
9
|
-
t.pattern = "spec/models/**/*_spec.rb"
|
10
|
-
end
|
11
|
-
|
12
|
-
Shaf::TestTask.new(:serializers) do |t|
|
13
|
-
t.pattern = "spec/serializers/**/*_spec.rb"
|
14
|
-
end
|
15
|
-
|
16
|
-
Shaf::TestTask.new(:lib) do |t|
|
17
|
-
t.pattern = "spec/lib/**/*_spec.rb"
|
18
|
-
end
|
19
|
-
|
20
|
-
Shaf::TestTask.new(:all) do |t|
|
21
|
-
t.pattern = [
|
22
|
-
"spec/lib/**/*_spec.rb",
|
23
|
-
"spec/models/**/*_spec.rb",
|
24
|
-
"spec/serializers/**/*_spec.rb",
|
25
|
-
"spec/integration/**/*_spec.rb"
|
26
|
-
]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
3
|
desc "Run all tests"
|
31
4
|
task test: 'test:all'
|
32
5
|
|
data/lib/shaf/router.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shaf/middleware'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module Shaf
|
7
|
+
class Router
|
8
|
+
class << self
|
9
|
+
def mount(controller, default: false)
|
10
|
+
@default_controller = controller if default
|
11
|
+
@controllers ||= []
|
12
|
+
@controllers << controller
|
13
|
+
end
|
14
|
+
|
15
|
+
def routes
|
16
|
+
init_routes unless defined? @routes
|
17
|
+
@routes
|
18
|
+
end
|
19
|
+
|
20
|
+
# This controller will be used when no other can handle the request
|
21
|
+
# (E.g. returning 404 Not Found)
|
22
|
+
def default_controller
|
23
|
+
@default_controller ||= nil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :controllers
|
29
|
+
|
30
|
+
def init_routes
|
31
|
+
@routes = {}
|
32
|
+
controllers.each { |controller| init_routes_for(controller) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def init_routes_for(controller)
|
36
|
+
controller.routes.each do |method, controller_routes|
|
37
|
+
routes[method] ||= Hash.new { |hash, key| hash[key] = [] }
|
38
|
+
routes[method][controller] += controller_routes.map(&:first)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(app)
|
44
|
+
@app = app
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(env)
|
48
|
+
http_method, path = http_details(env)
|
49
|
+
|
50
|
+
result = nil
|
51
|
+
|
52
|
+
controllers_for(http_method, path) do |controller|
|
53
|
+
result = controller.call(env)
|
54
|
+
break unless cascade? result
|
55
|
+
end
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def http_details(env)
|
63
|
+
[env['REQUEST_METHOD'], env['PATH_INFO']]
|
64
|
+
end
|
65
|
+
|
66
|
+
def controllers_for(http_method, path)
|
67
|
+
find_cached(http_method, path).each { |ctrlr| yield ctrlr }
|
68
|
+
|
69
|
+
if controller = find(http_method, path)
|
70
|
+
yield controller
|
71
|
+
end
|
72
|
+
|
73
|
+
find_all(http_method, path).each do |ctrlr|
|
74
|
+
yield ctrlr unless ctrlr == controller
|
75
|
+
end
|
76
|
+
|
77
|
+
yield default_controller
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_controller
|
81
|
+
self.class.default_controller || @app || raise('No default controller')
|
82
|
+
end
|
83
|
+
|
84
|
+
def routes
|
85
|
+
self.class.routes
|
86
|
+
end
|
87
|
+
|
88
|
+
def find(http_method, path)
|
89
|
+
routes[http_method].each do |controller, patterns|
|
90
|
+
next unless patterns.any? { |pattern| pattern.match(path) }
|
91
|
+
add_cache(controller, http_method, path)
|
92
|
+
return controller
|
93
|
+
end
|
94
|
+
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_all(http_method, path)
|
99
|
+
Set.new.tap do |controllers|
|
100
|
+
routes[http_method].each do |ctrlr, patterns|
|
101
|
+
next unless patterns.any? { |pattern| pattern.match(path) }
|
102
|
+
add_cache(ctrlr, http_method, path)
|
103
|
+
controllers << ctrlr
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def cascade?(result)
|
109
|
+
result.dig(1, 'X-Cascade') == 'pass'
|
110
|
+
end
|
111
|
+
|
112
|
+
def cache
|
113
|
+
@cache ||= Hash.new { |hash, key| hash[key] = Set.new }
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_cache(controller, http_method, path)
|
117
|
+
key = cache_key(http_method, path)
|
118
|
+
cache[key] << controller
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_cached(http_method, path)
|
122
|
+
key = cache_key(http_method, path)
|
123
|
+
cache[key]
|
124
|
+
end
|
125
|
+
|
126
|
+
def cache_key(http_method, path)
|
127
|
+
path[1..-1].split('/').inject("#{http_method}_") do |key, segment|
|
128
|
+
segment = ':id' if segment =~ /\A\d+\z/
|
129
|
+
"#{key}/#{segment}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/shaf/settings.rb
CHANGED
@@ -1,24 +1,50 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
module Shaf
|
5
6
|
class Settings
|
6
7
|
SETTINGS_FILE = 'config/settings.yml'
|
8
|
+
DEFAULTS = {
|
9
|
+
public_folder: 'frontend/assets',
|
10
|
+
views_folder: 'frontend/views',
|
11
|
+
documents_dir: 'doc/api',
|
12
|
+
migrations_dir: 'db/migrations',
|
13
|
+
fixtures_dir: 'spec/fixtures',
|
14
|
+
auth_token_header: 'X-Auth-Token',
|
15
|
+
paginate_per_page: 25
|
16
|
+
}.freeze
|
7
17
|
|
8
18
|
class << self
|
9
19
|
def load
|
10
|
-
@settings =
|
11
|
-
|
20
|
+
@settings = DEFAULTS.dup
|
21
|
+
config = read_config(SETTINGS_FILE)
|
22
|
+
@settings.merge! config.fetch(env, {})
|
23
|
+
end
|
24
|
+
|
25
|
+
def read_config(file)
|
26
|
+
return {} unless File.exist? file
|
27
|
+
|
28
|
+
yaml = File.read(file)
|
29
|
+
if RUBY_VERSION < '2.5.0'
|
30
|
+
YAML.safe_load(yaml, [], [], true).each_with_object({}) do |(k, v), hash|
|
31
|
+
hash[k.to_sym] = v
|
32
|
+
end
|
33
|
+
elsif RUBY_VERSION < '2.6.0'
|
34
|
+
YAML.safe_load(yaml, [], [], true).transform_keys(&:to_sym)
|
35
|
+
else
|
36
|
+
YAML.safe_load(yaml, aliases: true, symbolize_names: true)
|
37
|
+
end
|
12
38
|
end
|
13
39
|
|
14
40
|
def env
|
15
|
-
|
41
|
+
(ENV['APP_ENV'] || ENV['RACK_ENV'] || 'development').to_sym
|
16
42
|
end
|
17
43
|
|
18
44
|
def method_missing(method, *args)
|
19
45
|
load unless defined? @settings
|
20
46
|
|
21
|
-
if method.to_s.end_with?
|
47
|
+
if method.to_s.end_with? '='
|
22
48
|
define_setter(method)
|
23
49
|
public_send(method, args.first)
|
24
50
|
else
|
@@ -27,21 +53,20 @@ module Shaf
|
|
27
53
|
end
|
28
54
|
end
|
29
55
|
|
30
|
-
def respond_to_missing?(
|
31
|
-
|
56
|
+
def respond_to_missing?(_method, _include_private = false)
|
57
|
+
true
|
32
58
|
end
|
33
59
|
|
34
60
|
def define_getter(method)
|
35
61
|
define_singleton_method(method) do
|
36
|
-
@settings
|
62
|
+
@settings[method]
|
37
63
|
end
|
38
64
|
end
|
39
65
|
|
40
66
|
def define_setter(method)
|
67
|
+
key = method[0..-2].to_sym
|
41
68
|
define_singleton_method(method) do |arg|
|
42
|
-
key =
|
43
|
-
@settings[env.to_s] ||= {}
|
44
|
-
@settings[env.to_s][key] = arg
|
69
|
+
@settings[key] = arg
|
45
70
|
end
|
46
71
|
end
|
47
72
|
end
|