shaf 0.1.0.beta

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 (90) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +1 -0
  4. data/bin/shaf +57 -0
  5. data/lib/shaf.rb +9 -0
  6. data/lib/shaf/api_doc.rb +124 -0
  7. data/lib/shaf/api_doc/comment.rb +27 -0
  8. data/lib/shaf/api_doc/document.rb +133 -0
  9. data/lib/shaf/app.rb +22 -0
  10. data/lib/shaf/command.rb +42 -0
  11. data/lib/shaf/command/console.rb +17 -0
  12. data/lib/shaf/command/generate.rb +19 -0
  13. data/lib/shaf/command/new.rb +79 -0
  14. data/lib/shaf/command/server.rb +15 -0
  15. data/lib/shaf/command/templates/Gemfile.erb +30 -0
  16. data/lib/shaf/doc_model.rb +54 -0
  17. data/lib/shaf/errors.rb +77 -0
  18. data/lib/shaf/extensions.rb +11 -0
  19. data/lib/shaf/extensions/authorize.rb +42 -0
  20. data/lib/shaf/extensions/resource_uris.rb +153 -0
  21. data/lib/shaf/formable.rb +188 -0
  22. data/lib/shaf/generator.rb +69 -0
  23. data/lib/shaf/generator/controller.rb +106 -0
  24. data/lib/shaf/generator/migration.rb +122 -0
  25. data/lib/shaf/generator/migration/add_column.rb +49 -0
  26. data/lib/shaf/generator/migration/create_table.rb +40 -0
  27. data/lib/shaf/generator/migration/drop_column.rb +45 -0
  28. data/lib/shaf/generator/migration/empty.rb +21 -0
  29. data/lib/shaf/generator/migration/rename_column.rb +48 -0
  30. data/lib/shaf/generator/model.rb +68 -0
  31. data/lib/shaf/generator/policy.rb +43 -0
  32. data/lib/shaf/generator/scaffold.rb +26 -0
  33. data/lib/shaf/generator/serializer.rb +258 -0
  34. data/lib/shaf/generator/templates/api/controller.rb.erb +62 -0
  35. data/lib/shaf/generator/templates/api/model.rb.erb +20 -0
  36. data/lib/shaf/generator/templates/api/policy.rb.erb +26 -0
  37. data/lib/shaf/generator/templates/api/serializer.rb.erb +24 -0
  38. data/lib/shaf/generator/templates/spec/integration_spec.rb.erb +98 -0
  39. data/lib/shaf/generator/templates/spec/model.rb.erb +40 -0
  40. data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +46 -0
  41. data/lib/shaf/helpers.rb +15 -0
  42. data/lib/shaf/helpers/json_html.rb +65 -0
  43. data/lib/shaf/helpers/paginate.rb +24 -0
  44. data/lib/shaf/helpers/payload.rb +115 -0
  45. data/lib/shaf/helpers/session.rb +53 -0
  46. data/lib/shaf/middleware.rb +1 -0
  47. data/lib/shaf/middleware/request_id.rb +16 -0
  48. data/lib/shaf/registrable_factory.rb +71 -0
  49. data/lib/shaf/settings.rb +33 -0
  50. data/lib/shaf/spec.rb +6 -0
  51. data/lib/shaf/spec/http_method_utils.rb +24 -0
  52. data/lib/shaf/spec/integration_spec.rb +53 -0
  53. data/lib/shaf/spec/model.rb +17 -0
  54. data/lib/shaf/spec/payload_test.rb +78 -0
  55. data/lib/shaf/spec/payload_utils.rb +176 -0
  56. data/lib/shaf/spec/serializer_spec.rb +24 -0
  57. data/lib/shaf/tasks.rb +4 -0
  58. data/lib/shaf/tasks/db.rb +61 -0
  59. data/lib/shaf/tasks/test.rb +43 -0
  60. data/lib/shaf/utils.rb +53 -0
  61. data/lib/shaf/version.rb +3 -0
  62. data/templates/Rakefile +13 -0
  63. data/templates/api/controllers/base_controller.rb +57 -0
  64. data/templates/api/controllers/docs_controller.rb +16 -0
  65. data/templates/api/controllers/root_controller.rb +8 -0
  66. data/templates/api/serializers/error_serializer.rb +10 -0
  67. data/templates/api/serializers/form_serializer.rb +42 -0
  68. data/templates/api/serializers/root_serializer.rb +16 -0
  69. data/templates/config.ru +4 -0
  70. data/templates/config/bootstrap.rb +12 -0
  71. data/templates/config/constants.rb +5 -0
  72. data/templates/config/customize.rb +3 -0
  73. data/templates/config/database.rb +40 -0
  74. data/templates/config/directories.rb +32 -0
  75. data/templates/config/helpers.rb +18 -0
  76. data/templates/config/initializers.rb +12 -0
  77. data/templates/config/initializers/db_migrations.rb +18 -0
  78. data/templates/config/initializers/hal_presenter.rb +6 -0
  79. data/templates/config/initializers/logging.rb +7 -0
  80. data/templates/config/initializers/sequel.rb +4 -0
  81. data/templates/config/settings.yml +19 -0
  82. data/templates/frontend/assets/css/main.css +70 -0
  83. data/templates/frontend/views/form.erb +16 -0
  84. data/templates/frontend/views/layout.erb +11 -0
  85. data/templates/frontend/views/payload.erb +8 -0
  86. data/templates/spec/integration/root_spec.rb +14 -0
  87. data/templates/spec/serializers/root_serializer_spec.rb +12 -0
  88. data/templates/spec/spec_helper.rb +4 -0
  89. metadata +348 -0
  90. metadata.gz.sig +0 -0
@@ -0,0 +1,53 @@
1
+ require 'digest'
2
+
3
+ module Shaf
4
+ module Session
5
+
6
+ SESSION_TTL = 60 * 60 * 24 * 2 # 2 days
7
+
8
+ def login(email, password)
9
+ return unless email && password
10
+ user = User.first(email: email) or return
11
+ bcrypt = BCrypt::Password.new(user.password_digest)
12
+ return unless bcrypt == password
13
+ @current_user = user
14
+
15
+ Session.where(user_id: user.id).delete
16
+ params = {
17
+ user_id: user.id,
18
+ expire_at: Time.now + SESSION_TTL,
19
+ }
20
+ Session.create(params)
21
+ end
22
+
23
+ def extend_session(session)
24
+ return unless session
25
+ session.update(expire_at: Time.now + SESSION_TTL)
26
+ session.auth_token = request.env['HTTP_X_AUTH_TOKEN']
27
+ session
28
+ end
29
+
30
+ def logout
31
+ current_session&.destroy
32
+ end
33
+
34
+ def current_user
35
+ unless defined?(@current_user) && @current_user
36
+ return unless request.env.key? 'HTTP_X_AUTH_TOKEN'
37
+ digest = Digest::SHA256.hexdigest(request.env['HTTP_X_AUTH_TOKEN'])
38
+ session = Session.where(auth_token_digest: digest).first
39
+ @current_user = User[session.user_id] if session&.valid?
40
+ end
41
+ @current_user
42
+ end
43
+
44
+ def current_session
45
+ unless @current_session
46
+ return unless current_user
47
+ @current_session = Session.where(user_id: current_user.id).first
48
+ end
49
+ @current_session
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1 @@
1
+ require 'shaf/middleware/request_id'
@@ -0,0 +1,16 @@
1
+ require 'securerandom'
2
+
3
+ module Shaf
4
+ module Middleware
5
+ class RequestId
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env["REQUEST_ID"] = SecureRandom.uuid
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,71 @@
1
+ module Shaf
2
+ module RegistrableFactory
3
+
4
+ class NotFoundError < StandardError; end
5
+
6
+ def all
7
+ reg.dup
8
+ end
9
+
10
+ def size
11
+ reg.size
12
+ end
13
+
14
+ def register(clazz)
15
+ reg << clazz
16
+ end
17
+
18
+ def unregister(*str)
19
+ return if str.empty? || !str.all?
20
+ reg.delete_if { |clazz| matching_class? str, clazz }
21
+ end
22
+
23
+ def lookup(*str)
24
+ return if str.empty? || !str.all?
25
+ reg.detect { |clazz| matching_class? str, clazz }
26
+ end
27
+
28
+ def usage
29
+ reg.compact.map do |entry|
30
+ usage = entry.instance_eval { @usage }
31
+ usage.respond_to?(:call) ? usage.call : usage
32
+ end
33
+ end
34
+
35
+ def create(*params)
36
+ clazz = lookup(*params)
37
+ raise NotFoundError.new(%Q(Command '#{ARGV}' is not supported)) unless clazz
38
+
39
+ args = init_args(clazz, params)
40
+ clazz.new(*args)
41
+ end
42
+
43
+ private
44
+
45
+ def reg
46
+ @reg ||= []
47
+ end
48
+
49
+ def matching_class?(strings, clazz)
50
+ identifiers = clazz.instance_eval { @identifiers }
51
+ return false if strings.size < identifiers.size
52
+ identifiers.zip(strings).all? { |pattern, str| matching_identifier? str, pattern }
53
+ end
54
+
55
+ def matching_identifier?(str, pattern)
56
+ return false if pattern.nil? || str.nil? || str.empty?
57
+ pattern = pattern.to_s if pattern.is_a? Symbol
58
+ return str == pattern if pattern.is_a? String
59
+ str.match(pattern) || false
60
+ end
61
+
62
+ def identifier_count(clazz)
63
+ clazz.instance_eval { @identifiers }&.size || 0
64
+ end
65
+
66
+ def init_args(clazz, params)
67
+ first_non_id = identifier_count(clazz)
68
+ params[first_non_id..-1]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,33 @@
1
+ require 'yaml'
2
+
3
+
4
+ module Shaf
5
+ class Settings
6
+ SETTINGS_FILE = 'config/settings.yml'
7
+
8
+ class << self
9
+ def load
10
+ @settings = File.exist?(SETTINGS_FILE) ?
11
+ YAML.load(File.read(SETTINGS_FILE)) : {}
12
+ end
13
+
14
+ def env
15
+ @env ||= (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
16
+ end
17
+
18
+ def method_missing(method, *args)
19
+ load unless defined? @settings
20
+
21
+ define_singleton_method(method) do
22
+ @settings.dig(env.to_s, method.to_s)
23
+ end
24
+
25
+ return public_send(method)
26
+ end
27
+
28
+ def respond_to_missing?(method, include_private = false)
29
+ return true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,6 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/hooks'
3
+ require 'shaf/spec/http_method_utils'
4
+ require 'shaf/spec/payload_utils'
5
+ require 'shaf/spec/integration_spec'
6
+ require 'shaf/spec/serializer_spec'
@@ -0,0 +1,24 @@
1
+ module Shaf
2
+ module Spec
3
+ module HttpMethodUtils
4
+ include ::Rack::Test::Methods
5
+
6
+ [:get, :put, :post, :delete].each do |m|
7
+ define_method m do |*args|
8
+ set_headers
9
+ super(*args)
10
+ set_payload parse_response(last_response.body)
11
+ end
12
+ end
13
+
14
+ def status
15
+ last_response&.status
16
+ end
17
+
18
+ def headers
19
+ last_response&.headers
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ module Shaf
2
+ module Spec
3
+ class IntegrationSpec < Minitest::Spec
4
+ include Minitest::Hooks
5
+ include HttpMethodUtils
6
+ include PayloadUtils
7
+ include UriHelper
8
+
9
+ TRANSACTION_OPTIONS = {
10
+ rollback: :always,
11
+ savepoint: true,
12
+ auto_savepoint: true
13
+ }.freeze
14
+
15
+ register_spec_type self do |desc, args|
16
+ next unless args && args.is_a?(Hash)
17
+ args[:type]&.to_s == 'integration'
18
+ end
19
+
20
+ around do |&block|
21
+ DB.transaction(TRANSACTION_OPTIONS) { super(&block) }
22
+ end
23
+
24
+ def set_headers
25
+ if defined?(@__integration_test_auth_token) && @__integration_test_auth_token
26
+ header 'X-AUTH-TOKEN', @__integration_test_auth_token
27
+ end
28
+ end
29
+
30
+ def parse_response(body)
31
+ return nil if body.empty?
32
+ JSON.parse(body, symbolize_names: true)
33
+ end
34
+
35
+ def app
36
+ App.instance
37
+ end
38
+
39
+ # def login(email, pass)
40
+ # params = {email: email, password: pass}
41
+ # header 'Content-Type', 'application/json'
42
+ # post Shaf::UriHelper.session_uri, JSON.generate(params)
43
+ # @__integration_test_auth_token = attribute[:auth_token]
44
+ # end
45
+ #
46
+ # def logout
47
+ # delete Shaf::UriHelper.session_uri
48
+ # @__integration_test_auth_token = nil
49
+ # end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ module TestUtils
2
+ module Model
3
+ class Request
4
+ attr_reader :env
5
+
6
+ def env
7
+ @env ||= {}
8
+ end
9
+ end
10
+
11
+ module Test
12
+ def request
13
+ @request ||= Request.new
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,78 @@
1
+ $:.unshift '.'
2
+ require 'minitest/autorun'
3
+ require 'test/test_utils/payload'
4
+
5
+ class PayloadTest < Minitest::Test
6
+ include TestUtils::Payload
7
+
8
+ def setup
9
+ @payload = {
10
+ attr1: 1,
11
+ attr2: 2,
12
+ _links: {
13
+ link1: {
14
+ href: '/link1'
15
+ },
16
+ link2: {
17
+ href: '/link2'
18
+ }
19
+ },
20
+ _embedded: {
21
+ embed1: {
22
+ attr_e1: 'e1',
23
+ _links: {
24
+ link_e1: {
25
+ href: '/links/e1'
26
+ }
27
+ },
28
+ _embedded: {
29
+ embed1a: {
30
+ attr_e1a: 'e1a'
31
+ }
32
+ }
33
+ },
34
+ embed2: [
35
+ {
36
+ attr_embed2a: 'e2a',
37
+ _links: {
38
+ link_e2a: {
39
+ href: 'links/e2a'
40
+ }
41
+ }
42
+ },
43
+ {
44
+ attr_embed2b: 'e2b',
45
+ _links: {
46
+ link_e2b: {
47
+ href: 'links/e2b'
48
+ }
49
+ }
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ end
55
+
56
+ #TODO How to verify that we do get assertions
57
+
58
+ def test_links
59
+ assert_link :link1, '/link1'
60
+ assert_link :link2, '/link2'
61
+ end
62
+
63
+ def test_embedded_without_block
64
+ assert_equal @payload[:_embedded], embedded
65
+ assert_equal @payload[:_embedded][:embed1], embedded(:embed1)
66
+ end
67
+
68
+ def test_embedded_with_block
69
+ embedded :embed1 do
70
+ assert_attribute :attr_e1, 'e1'
71
+ assert_link :link_e1, '/links/e1'
72
+
73
+ embedded :embed1a do
74
+ assert_attribute :attr_e1a, 'e1a'
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,176 @@
1
+ require 'minitest/assertions'
2
+
3
+ module Shaf
4
+ module Spec
5
+ module PayloadUtils
6
+
7
+ class Embedded
8
+ include HttpMethodUtils
9
+ include PayloadUtils
10
+ include Minitest::Assertions
11
+
12
+ # This is needed by Minitest::Assertions
13
+ # And we need that module to have all assert_*/refute_* methods
14
+ # available in this class
15
+ attr_accessor :assertions
16
+
17
+ def initialize(payload, context, block)
18
+ @payload = payload
19
+ @context = context
20
+ @block = block
21
+ @assertions = 0
22
+ end
23
+
24
+ def call
25
+ instance_exec(&@block)
26
+ end
27
+
28
+ def method_missing(method, *args, &block)
29
+ if @context&.respond_to? method
30
+ define_singleton_method(method) { |*a, &b| @context.public_send method, *a, &b }
31
+ return public_send(method, *args, &block)
32
+ end
33
+ super
34
+ end
35
+
36
+ def respond_to_missing?(method, include_private = false)
37
+ return true if @context&.respond_to? method
38
+ super
39
+ end
40
+
41
+ end
42
+
43
+ def set_payload(payload)
44
+ @payload = payload
45
+ @payload = JSON.parse(payload, symbolize_names: true) if payload.is_a?(String)
46
+ end
47
+
48
+ def last_payload
49
+ refute @payload.nil?, "No previous response body"
50
+ @payload
51
+ end
52
+
53
+ def attributes
54
+ last_payload.reject { |key,_| [:_links, :_embedded].include? key }
55
+ end
56
+
57
+ def links
58
+ last_payload[:_links] || []
59
+ end
60
+
61
+ def link_rels
62
+ links.keys
63
+ end
64
+
65
+ def embedded_resources
66
+ last_payload[:_embedded]&.keys || []
67
+ end
68
+
69
+ def embedded(name = nil)
70
+ assert_has_embedded name
71
+ keys = [:_embedded, name&.to_sym].compact
72
+ return last_payload.dig(*keys) unless block_given?
73
+ Embedded.new(last_payload.dig(*keys), self, Proc.new).call
74
+ end
75
+
76
+ def follow_rel(rel, method: nil)
77
+ assert_has_link(rel)
78
+ link = links[rel.to_sym]
79
+ if method && respond_to?(method)
80
+ public_send(method, link[:href])
81
+ else
82
+ get link[:href]
83
+ end
84
+ end
85
+
86
+ def fill_form(fields)
87
+ fields.map do |field|
88
+ value = case field[:type]
89
+ when 'integer'
90
+ field[:name].size
91
+ when 'string'
92
+ "value for #{field[:name]}"
93
+ else
94
+ "type not supported"
95
+ end
96
+ [field[:name], value]
97
+ end.to_h
98
+ end
99
+
100
+ def assert_status(code)
101
+ assert_equal code, status,
102
+ "Response status was expected to be #{code}."
103
+ end
104
+
105
+ def assert_header(key, value)
106
+ assert_equal value, headers[key],
107
+ "Response was expected have header #{key} = #{value}."
108
+ end
109
+
110
+ def assert_has_attribute(attr)
111
+ assert last_payload[attr.to_sym],
112
+ "Response does not contain attribute '#{attr}': #{last_payload}"
113
+ end
114
+
115
+ def refute_has_attribute(attr)
116
+ refute last_payload[attr.to_sym],
117
+ "Response contains disallowed attribute '#{attr}': #{last_payload}"
118
+ end
119
+
120
+ def assert_has_attributes(*attrs)
121
+ attrs.each { |attr| assert_has_attribute(attr) }
122
+ end
123
+
124
+ def refute_has_attributes(*attrs)
125
+ attrs.each { |attr| refute_has_attribute(attr) }
126
+ end
127
+
128
+ def assert_attribute(attr, expected)
129
+ assert_has_attribute(attr)
130
+ assert_equal expected, last_payload[attr.to_sym]
131
+ end
132
+
133
+ def assert_has_link(rel)
134
+ assert last_payload.key?(:_links), "Response does not have any links: #{last_payload}"
135
+ assert last_payload[:_links][rel.to_sym],
136
+ "Response does not contain link with rel '#{rel}': #{last_payload}"
137
+ assert last_payload[:_links][rel.to_sym][:href],
138
+ "link with rel '#{rel}' in ressponse does not have a href: #{last_payload}"
139
+ end
140
+
141
+ def refute_has_link(rel)
142
+ refute last_payload.dig(:_links, rel.to_sym),
143
+ "Response contains disallowed link with rel '#{rel}': #{last_payload}"
144
+ end
145
+
146
+ def assert_has_links(*rels)
147
+ rels.each { |rel| assert_has_link(rel) }
148
+ end
149
+
150
+ def refute_has_links(*rels)
151
+ rels.each { |rel| refute_has_link(rel) }
152
+ end
153
+
154
+ def assert_link(rel, expected)
155
+ assert_has_link(rel)
156
+ assert_equal expected, last_payload.dig(:_links, rel.to_sym, :href)
157
+ end
158
+
159
+ def assert_has_embedded(*names)
160
+ names.each do |name|
161
+ assert last_payload.key?(:_embedded),
162
+ "Response does not have any embedded resources: #{last_payload}"
163
+ assert last_payload[:_embedded][name.to_sym],
164
+ "Response does not contain embedded resource with name '#{name}': #{last_payload}"
165
+ end
166
+ end
167
+
168
+ def refute_has_embedded(*names)
169
+ names.each do |name|
170
+ refute last_payload.dig(:_embedded, name.to_sym),
171
+ "Response contains disallowed embedded resource with name '#{name}': #{last_payload}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end