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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/bin/shaf +19 -4
  5. data/lib/shaf.rb +1 -0
  6. data/lib/shaf/app.rb +7 -10
  7. data/lib/shaf/command/server.rb +4 -2
  8. data/lib/shaf/errors.rb +22 -2
  9. data/lib/shaf/extensions/controller_hooks.rb +14 -0
  10. data/lib/shaf/extensions/resource_uris.rb +1 -1
  11. data/lib/shaf/formable/field.rb +10 -6
  12. data/lib/shaf/formable/form.rb +4 -0
  13. data/lib/shaf/generator/base.rb +2 -2
  14. data/lib/shaf/generator/controller.rb +11 -11
  15. data/lib/shaf/generator/helper.rb +33 -0
  16. data/lib/shaf/generator/migration.rb +6 -1
  17. data/lib/shaf/generator/migration/empty.rb +2 -2
  18. data/lib/shaf/generator/serializer.rb +4 -4
  19. data/lib/shaf/generator/templates/api/controller.rb.erb +4 -3
  20. data/lib/shaf/generator/templates/api/serializer.rb.erb +10 -12
  21. data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +4 -2
  22. data/lib/shaf/helpers.rb +2 -0
  23. data/lib/shaf/helpers/http_header.rb +26 -0
  24. data/lib/shaf/helpers/paginate.rb +4 -2
  25. data/lib/shaf/helpers/payload.rb +33 -8
  26. data/lib/shaf/middleware/request_id.rb +3 -1
  27. data/lib/shaf/rake/db.rb +5 -5
  28. data/lib/shaf/rake/test.rb +0 -27
  29. data/lib/shaf/router.rb +133 -0
  30. data/lib/shaf/settings.rb +36 -11
  31. data/lib/shaf/spec/http_method_utils.rb +11 -3
  32. data/lib/shaf/spec/integration_spec.rb +4 -4
  33. data/lib/shaf/spec/serializer_spec.rb +1 -1
  34. data/lib/shaf/upgrade/manifest.rb +34 -9
  35. data/lib/shaf/upgrade/package.rb +24 -15
  36. data/lib/shaf/utils.rb +31 -21
  37. data/lib/shaf/version.rb +1 -1
  38. data/templates/Rakefile +27 -0
  39. data/templates/api/controllers/base_controller.rb +13 -8
  40. data/templates/api/serializers/error_serializer.rb +3 -1
  41. data/templates/api/serializers/form_serializer.rb +11 -16
  42. data/templates/api/serializers/validation_error_serializer.rb +13 -0
  43. data/templates/config.ru +1 -1
  44. data/templates/config/bootstrap.rb +3 -1
  45. data/templates/config/database.rb +6 -7
  46. data/templates/config/directories.rb +3 -2
  47. data/templates/config/helpers.rb +1 -1
  48. data/templates/config/initializers/db_migrations.rb +14 -8
  49. data/templates/config/initializers/logging.rb +2 -2
  50. data/templates/config/initializers/sequel.rb +1 -0
  51. data/templates/config/paths.rb +7 -0
  52. data/templates/config/settings.yml +5 -0
  53. data/upgrades/1.0.0.tar.gz +0 -0
  54. metadata +38 -19
  55. metadata.gz.sig +0 -0
  56. 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
- <% attributes_with_doc.each do |attr| %>
9
- <%= attr.join("\n ") %>
10
- <% end %>
11
- <% curies_with_doc.each do |curie| %>
12
- <%= curie.join("\n ") %>
13
- <% end %>
14
- <% links_with_doc.each do |link| %>
15
- <%= link.join("\n ") %>
16
- <% end %>
17
- <% if collection_with_doc %>
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
- set_payload HALPresenter.to_hal(resource)
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
- set_payload HALPresenter.to_hal(resource, current_user: "Bengt")
32
+ serialize resource, current_user: user
31
33
  end
32
34
 
33
35
  it "serializes attributes" do
@@ -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 = PAGINATION_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 = PAGINATION_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
@@ -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
- field_strings = fields.map { |f| f.to_s.downcase }
62
- field_strings << 'id' unless field_strings.include? 'id'
65
+
66
+ fields = fields.map { |f| f.to_sym.downcase }.to_set
67
+ fields << :id
63
68
 
64
69
  {}.tap do |allowed|
65
- field_strings.each do |f|
66
- allowed[f.to_sym] = payload[f] if payload[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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Shaf
@@ -8,7 +10,7 @@ module Shaf
8
10
  end
9
11
 
10
12
  def call(env)
11
- env["REQUEST_ID"] = SecureRandom.uuid
13
+ env['REQUEST_ID'] = SecureRandom.uuid
12
14
  @app.call(env)
13
15
  end
14
16
  end
@@ -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, MIGRATIONS_DIR, target: args[:version].to_i)
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, MIGRATIONS_DIR)
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, MIGRATIONS_DIR, target: version.to_i)
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, MIGRATIONS_DIR, target: version)
63
- Sequel::Migrator.run(DB, MIGRATIONS_DIR)
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
@@ -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
 
@@ -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
@@ -1,24 +1,50 @@
1
- require 'yaml'
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 = File.exist?(SETTINGS_FILE) ?
11
- YAML.load(File.read(SETTINGS_FILE)) : {}
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
- @env ||= (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
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?(method, include_private = false)
31
- return true
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.dig(env.to_s, method.to_s)
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 = method[0..-2]
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