shaf 0.8.0 → 1.0.0

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