shaf 0.1.0.beta

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