shaf 0.3.1 → 0.4.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 (45) 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 +3 -0
  5. data/lib/shaf.rb +1 -0
  6. data/lib/shaf/api_doc/document.rb +0 -1
  7. data/lib/shaf/app.rb +1 -1
  8. data/lib/shaf/command.rb +19 -2
  9. data/lib/shaf/command/generate.rb +11 -1
  10. data/lib/shaf/command/new.rb +1 -6
  11. data/lib/shaf/command/server.rb +7 -0
  12. data/lib/shaf/command/upgrade.rb +38 -0
  13. data/lib/shaf/extensions/authorize.rb +8 -6
  14. data/lib/shaf/extensions/resource_uris.rb +129 -71
  15. data/lib/shaf/generator.rb +4 -0
  16. data/lib/shaf/generator/controller.rb +2 -2
  17. data/lib/shaf/generator/migration.rb +33 -14
  18. data/lib/shaf/generator/migration/add_column.rb +1 -3
  19. data/lib/shaf/generator/migration/add_index.rb +42 -0
  20. data/lib/shaf/generator/model.rb +4 -4
  21. data/lib/shaf/generator/policy.rb +1 -1
  22. data/lib/shaf/generator/scaffold.rb +3 -3
  23. data/lib/shaf/generator/serializer.rb +5 -5
  24. data/lib/shaf/rake.rb +5 -0
  25. data/lib/shaf/rake/db.rb +79 -0
  26. data/lib/shaf/rake/test.rb +32 -0
  27. data/lib/shaf/registrable_factory.rb +8 -3
  28. data/lib/shaf/settings.rb +20 -4
  29. data/lib/shaf/tasks.rb +6 -3
  30. data/lib/shaf/{api_doc/task.rb → tasks/api_doc_task.rb} +6 -5
  31. data/lib/shaf/tasks/db_task.rb +42 -0
  32. data/lib/shaf/tasks/test_task.rb +16 -0
  33. data/lib/shaf/upgrade.rb +3 -0
  34. data/lib/shaf/upgrade/manifest.rb +31 -0
  35. data/lib/shaf/upgrade/package.rb +158 -0
  36. data/lib/shaf/upgrade/version.rb +52 -0
  37. data/lib/shaf/utils.rb +30 -0
  38. data/lib/shaf/version.rb +1 -1
  39. data/templates/Rakefile +2 -3
  40. data/templates/config/database.rb +3 -3
  41. metadata +33 -202
  42. metadata.gz.sig +0 -0
  43. data/lib/shaf/api_doc.rb +0 -3
  44. data/lib/shaf/tasks/db.rb +0 -81
  45. data/lib/shaf/tasks/test.rb +0 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d610f6b52c0b849399c31536099eb1aecfde8b0907ee7bb1839b3430b0ccdb12
4
- data.tar.gz: '069c31284757c68cf40538cbf623e3767847420b67cef09084f7461360978508'
3
+ metadata.gz: 749585960501bd61a7e7f4f22c2a90c2108f86c6cf0698ec87f495bb78d87389
4
+ data.tar.gz: da5fc9e7f87876f12654d5ea8473e7f0cd4a9dc3daba88055a51f7aaf0a794a5
5
5
  SHA512:
6
- metadata.gz: b0b6fa7cf8e841b6791bfe2aa271ce3cb752c8efd603299d56c999b8b86df0d62ff03bd8b8cf00ab215bdaab17178b8ced05af25da5f7298bd998b79c404eed4
7
- data.tar.gz: dedfaac82ee3fb083733354d41e4fb1417484868afb8ab956e072bf436b6035ac02287abb3b5a82d69af77b6d91da47e6eade4463dd13f1ef1c7ff98e72a429d
6
+ metadata.gz: '017559afb07a1d9e142a96c09c406cd697b6c797732661c8f431a1b2803c5c9e8c9c04cd7718037638a440ad077a55eaf1b382a9cdb47687b4bef7921b6ae4c5'
7
+ data.tar.gz: 3d2a16944a04292fd9039b2dbe5f482950296b7f1dd853e383cdbed972f33755243b16a89b5b081f53da43d456c4b1c8f081ac31ecdbcea66fce3076b2b6ec78
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/bin/shaf CHANGED
@@ -24,6 +24,9 @@ module Shaf
24
24
  STDERR.puts "This command can only be executed inside a Shaf project directory. " \
25
25
  "Please change directory and try again!"
26
26
  exit 2
27
+ rescue Command::CommandError => e
28
+ STDERR.puts "Command failed: #{e.message}\n"
29
+ exit 3
27
30
  end
28
31
 
29
32
  def show_help
data/lib/shaf.rb CHANGED
@@ -7,3 +7,4 @@ require 'shaf/formable'
7
7
  require 'shaf/extensions'
8
8
  require 'shaf/helpers'
9
9
  require 'shaf/doc_model'
10
+ require 'shaf/upgrade'
@@ -2,7 +2,6 @@ require 'fileutils'
2
2
  require 'yaml'
3
3
  require 'redcarpet'
4
4
  require 'redcarpet/render_strip'
5
- require 'shaf/api_doc/comment'
6
5
 
7
6
  module Shaf
8
7
  module ApiDoc
data/lib/shaf/app.rb CHANGED
@@ -10,7 +10,7 @@ module Shaf
10
10
 
11
11
  def create_instance
12
12
  @instance = Sinatra.new
13
- @instance.set :port, Shaf::Settings.port || 3000
13
+ @instance.set :port, Settings.port || 3000
14
14
  @instance.use Shaf::Middleware::RequestId
15
15
  end
16
16
 
data/lib/shaf/command.rb CHANGED
@@ -1,10 +1,12 @@
1
+ require 'optparse'
1
2
  require 'shaf/utils'
2
3
  require 'shaf/registrable_factory'
3
4
 
4
5
  module Shaf
5
6
  module Command
6
7
 
7
- class ArgumentError < StandardError; end
8
+ class CommandError < StandardError; end
9
+ class ArgumentError < CommandError; end
8
10
 
9
11
  class Factory
10
12
  extend RegistrableFactory
@@ -13,7 +15,7 @@ module Shaf
13
15
  class Base
14
16
  include Utils
15
17
 
16
- attr_reader :args
18
+ attr_reader :args, :options
17
19
 
18
20
  class << self
19
21
  def inherited(child)
@@ -32,16 +34,31 @@ module Shaf
32
34
  STDERR.puts msg
33
35
  exit status
34
36
  end
37
+
38
+ def options(option_parser, options); end
35
39
  end
36
40
 
37
41
  def initialize(*args)
38
42
  @args = args.dup
43
+ @options = {}
44
+ parse_options!
45
+ end
46
+
47
+ private
48
+
49
+ def parse_options!
50
+ parser = OptionParser.new
51
+ self.class.options(parser, @options)
52
+ parser.parse!(args)
53
+ rescue OptionParser::InvalidOption => e
54
+ raise ArgumentError, e.message
39
55
  end
40
56
  end
41
57
  end
42
58
  end
43
59
 
44
60
  require 'shaf/command/new'
61
+ require 'shaf/command/upgrade'
45
62
  require 'shaf/command/server'
46
63
  require 'shaf/command/console'
47
64
  require 'shaf/command/generate'
@@ -7,9 +7,19 @@ module Shaf
7
7
  identifier %r(\Ag(en(erate)?)?\Z)
8
8
  usage Generator::Factory.usage.flatten.sort
9
9
 
10
+ def self.options(parser, options)
11
+ parser.on("-s", "--[no-]specs", "generate specs") do |s|
12
+ options[:specs] = s
13
+ end
14
+
15
+ Generator::Factory.each do |clazz|
16
+ clazz.options(parser, options)
17
+ end
18
+ end
19
+
10
20
  def call
11
21
  in_project_root do
12
- Generator::Factory.create(*args).call
22
+ Generator::Factory.create(*args).call(options)
13
23
  end
14
24
  rescue StandardError => e
15
25
  raise Command::ArgumentError, e.message
@@ -21,7 +21,7 @@ module Shaf
21
21
  Dir.chdir(@project_name) do
22
22
  copy_templates
23
23
  create_gemfile
24
- create_shaf_version_file
24
+ write_shaf_version
25
25
  create_ruby_version_file
26
26
  end
27
27
  end
@@ -46,11 +46,6 @@ module Shaf
46
46
  end
47
47
  end
48
48
 
49
- def create_shaf_version_file
50
- File.write '.shaf',
51
- YAML.dump({'version' => Shaf::VERSION})
52
- end
53
-
54
49
  def create_ruby_version_file
55
50
  File.write '.ruby-version', RUBY_VERSION
56
51
  end
@@ -5,7 +5,14 @@ module Shaf
5
5
  identifier %r(\As(erver)?\Z)
6
6
  usage 'server'
7
7
 
8
+ def self.options(parser, options)
9
+ parser.on("-p", "--port PORT", Integer, "Listen port") do |p|
10
+ options[:port] = p
11
+ end
12
+ end
13
+
8
14
  def call
15
+ Settings.port = options[:port] if options[:port]
9
16
  bootstrap
10
17
  App.instance.run!
11
18
  end
@@ -0,0 +1,38 @@
1
+ require 'shaf/upgrade'
2
+
3
+ module Shaf
4
+ module Command
5
+ class Upgrade < Base
6
+
7
+ class UnknownShafVersionError < CommandError; end
8
+ class UpgradeFailedError < CommandError; end
9
+
10
+ identifier %r(\Aupgrade\Z)
11
+ usage 'upgrade'
12
+
13
+ def call
14
+ in_project_root do
15
+ upgrade_packages.each { |package| apply(package) }
16
+ puts "\nProject is up-to-date! Shaf version: #{current_version}"
17
+ end
18
+ end
19
+
20
+ def apply(package)
21
+ package.load.apply or raise UpgradeFailedError
22
+ write_shaf_version package.version.to_s
23
+ rescue Errno::ENOENT
24
+ raise UpgradeFailedError,
25
+ "Failed to execute system command 'patch'. Make sure that 'patch' installed!" \
26
+ " (E.g. `sudo apt install patch` for Ubuntu)"
27
+ end
28
+
29
+ def upgrade_packages
30
+ Shaf::Upgrade::Package.all.select { |package| package > current_version }
31
+ end
32
+
33
+ def current_version
34
+ read_shaf_version or raise UnknownShafVersionError
35
+ end
36
+ end
37
+ end
38
+ end
@@ -18,12 +18,6 @@ module Shaf
18
18
  end
19
19
 
20
20
  module Helpers
21
- def policy(resource)
22
- return @policy if defined?(@policy) && @policy
23
- user = current_user if respond_to? :current_user
24
- @policy = self.class.policy_class&.new(user, resource)
25
- end
26
-
27
21
  def authorize(action, resource = nil)
28
22
  policy(resource) or raise Authorize::NoPolicyError
29
23
  @policy.public_send method_for(action)
@@ -33,6 +27,14 @@ module Shaf
33
27
  authorize(action, resource) or raise Authorize::PolicyViolationError
34
28
  end
35
29
 
30
+ private
31
+
32
+ def policy(resource)
33
+ return @policy if defined?(@policy) && @policy
34
+ user = current_user if respond_to? :current_user
35
+ @policy = self.class.policy_class&.new(user, resource)
36
+ end
37
+
36
38
  def method_for(action)
37
39
  return action if action.to_s.end_with? '?'
38
40
  "#{action}?".to_sym
@@ -54,10 +54,53 @@ module Shaf
54
54
  # edit_book_uri_template => /books/:id/edit
55
55
  #
56
56
  class CreateUriMethods
57
+
58
+ # Resources should never be nested more than 1 level deep.
59
+ MAX_NESTING_DEPTH = 1
60
+
61
+ class UriTemplateNestingError < StandardError
62
+ def initialize(msg = nil)
63
+ msg ||= "Uri templates only supports a nesting depth of #{MAX_NESTING_DEPTH}"
64
+ super(msg)
65
+ end
66
+ end
67
+
68
+ class UriTemplateVariableError < StandardError
69
+ def initialize(msg = nil)
70
+ msg ||= "Mismatch between uri templates and resources"
71
+ super(msg)
72
+ end
73
+ end
74
+
75
+ class << self
76
+ def resource_helper_uri(template_uri, *resources, query)
77
+ uri = replace_templates(template_uri, *resources)
78
+ query_string = MethodBuilder.query_string(query)
79
+ "#{uri}#{query_string}".freeze
80
+ end
81
+
82
+ def replace_templates(template_uri, *resources)
83
+ symbols = MethodBuilder.extract_symbols(template_uri)
84
+ resources.compact!
85
+ raise UriTemplateVariableError if symbols.size != resources.size
86
+
87
+ MethodBuilder.transform_symbols(template_uri) do |segment|
88
+ resrc = resources.shift
89
+ sym = symbols.shift
90
+ resrc.respond_to?(sym) ? resrc.public_send(sym) : resrc
91
+ end
92
+ end
93
+ end
94
+
57
95
  def initialize(name, base: nil, plural_name: nil)
58
96
  @name = name.to_s
59
97
  @base = base&.sub(%r(/\Z), '') || ''
60
- @plural_name = plural_name&.to_s || name.to_s + 's'
98
+ @plural_name = plural_name&.to_s || Utils::pluralize(name.to_s)
99
+
100
+ if nesting_depth > MAX_NESTING_DEPTH
101
+ raise UriTemplateNestingError,
102
+ "Too deep nesting level (max #{MAX_NESTING_DEPTH}): #{@base}"
103
+ end
61
104
  end
62
105
 
63
106
  def call
@@ -76,88 +119,101 @@ module Shaf
76
119
  attr_reader :name, :base, :plural_name
77
120
 
78
121
  def register_resources_uri
79
- uri = "#{base}/#{plural_name}"
80
- helper_name = "#{plural_name}_uri"
81
-
82
- UriHelperMethods.register(helper_name) do |**query|
83
- query_string = MethodBuilder.query_string(query)
84
- "#{uri}#{query_string}".freeze
85
- end
86
- UriHelperMethods.register("#{helper_name}_template") { uri.freeze }
122
+ template_uri = "#{base}/#{plural_name}".freeze
123
+ helper_name = "#{plural_name}_uri".freeze
124
+ register(template_uri, helper_name)
87
125
  end
88
126
 
89
127
  def register_resource_uri
90
- uri = "#{base}/#{plural_name}"
91
- helper_name = "#{name}_uri"
92
-
93
- UriHelperMethods.register helper_name do |resrc, **query|
94
- id = resrc.is_a?(Integer) ? resrc : resrc&.id
95
- query_string = MethodBuilder.query_string(query)
96
-
97
- next "#{uri}/#{id}#{query_string}".freeze if id.is_a?(Integer)
98
- raise ArgumentError,
99
- "In #{helper_name}: id must be an integer! was #{id.class}"
100
- end
101
-
102
- UriHelperMethods.register "#{helper_name}_template" do
103
- "#{uri}/:id".freeze
104
- end
128
+ template_uri = "#{base}/#{plural_name}/:id".freeze
129
+ helper_name = "#{name}_uri".freeze
130
+ register(template_uri, helper_name)
105
131
  end
106
132
 
107
133
  # If a resource has the same singular and plural names, then this method
108
134
  # should be used. It will return the resource uri when a resource is given
109
135
  # as argument and the resources uri when no arguments are provided.
110
136
  def register_resource_uri_by_arg
111
- uri = "#{base}/#{plural_name}"
137
+ resource_template_uri = "#{base}/#{plural_name}/:id"
138
+ collection_template_uri = "#{base}/#{plural_name}"
112
139
  helper_name = "#{plural_name}_uri"
113
140
 
114
- UriHelperMethods.register helper_name do |resrc = nil, **query|
115
- query_string = MethodBuilder.query_string(query)
116
-
117
- if resrc.nil?
118
- "#{uri}#{query_string}".freeze
119
- else
120
- id = (resrc.is_a?(Integer) ? resrc : resrc.id).to_i
121
-
122
- next "#{uri}/#{id}#{query_string}".freeze if id.is_a?(Integer)
123
- raise ArgumentError,
124
- "In #{helper_name}: id must be an integer! was #{id.class}"
125
- end
126
- end
127
-
128
- UriHelperMethods.register "#{helper_name}_template" do |collection = false|
129
- (collection ? uri : "#{uri}/:id").freeze
141
+ block = resource_or_collection_method_proc(resource_template_uri, collection_template_uri)
142
+ UriHelperMethods.register(helper_name, &block)
143
+ UriHelperMethods.register("#{helper_name}_template") do |collection = false|
144
+ (collection ? collection_template_uri : resource_template_uri).freeze
130
145
  end
131
146
  end
132
147
 
133
148
  def register_new_resource_uri
134
- uri = "#{base}/#{plural_name}/form"
135
- helper_name = "new_#{name}_uri"
136
-
137
- UriHelperMethods.register(helper_name) do |**query|
138
- query_string = MethodBuilder.query_string(query)
139
- "#{uri}#{query_string}".freeze
140
- end
141
- UriHelperMethods.register("#{helper_name}_template") { uri.freeze }
149
+ template_uri = "#{base}/#{plural_name}/form".freeze
150
+ helper_name = "new_#{name}_uri".freeze
151
+ register(template_uri, helper_name)
142
152
  end
143
153
 
144
154
  def register_edit_resource_uri
145
- uri = "#{base}/#{plural_name}"
146
- helper_name = "edit_#{name}_uri"
155
+ template_uri = "#{base}/#{plural_name}/:id/edit".freeze
156
+ helper_name = "edit_#{name}_uri".freeze
157
+ register(template_uri, helper_name)
158
+ end
147
159
 
148
- UriHelperMethods.register helper_name do |resrc, **query|
149
- id = resrc.is_a?(Integer) ? resrc : resrc&.id
150
- query_string = MethodBuilder.query_string(query)
160
+ def register(template_uri, helper_name)
161
+ block = resource_helper_method_proc(template_uri)
162
+ UriHelperMethods.register(helper_name, &block)
163
+ UriHelperMethods.register("#{helper_name}_template") { template_uri }
164
+ end
165
+
166
+ def resource_helper_method_proc(template_uri)
167
+ arg_count = MethodBuilder.extract_symbols(template_uri).size
151
168
 
152
- next "#{uri}/#{id}/edit#{query_string}".freeze if id.is_a?(Integer)
153
- raise ArgumentError,
154
- "In #{helper_name}: id must be an integer! was #{id.class}"
169
+ case arg_count
170
+ when 0
171
+ ->(**query) do
172
+ CreateUriMethods.resource_helper_uri(template_uri, query)
173
+ end
174
+ when 1
175
+ ->(resrc, **query) do
176
+ CreateUriMethods.resource_helper_uri(template_uri, resrc, query)
177
+ end
178
+ when 2
179
+ ->(parent_resrc, resrc, **query) do
180
+ CreateUriMethods.resource_helper_uri(template_uri, parent_resrc, resrc, query)
181
+ end
182
+ else
183
+ raise UriTemplateNestingError,
184
+ "Too deep nesting level (max #{MAX_NESTING_DEPTH}): #{template_uri}"
155
185
  end
186
+ end
156
187
 
157
- UriHelperMethods.register "#{helper_name}_template" do
158
- "#{uri}/:id/edit".freeze
188
+ def resource_or_collection_method_proc(resource_template_uri, collection_template_uri)
189
+ case nesting_depth
190
+ when 0
191
+ ->(resrc = nil, **query) {
192
+ args = if resrc.nil?
193
+ [collection_template_uri, query]
194
+ else
195
+ [resource_template_uri, resrc, query]
196
+ end
197
+ CreateUriMethods.resource_helper_uri(*args)
198
+ }
199
+ when 1
200
+ ->(parent_resrc, resrc = nil, **query) {
201
+ args = if resrc.nil?
202
+ [collection_template_uri, parent_resrc, query]
203
+ else
204
+ [resource_template_uri, parent_resrc, resrc, query]
205
+ end
206
+ CreateUriMethods.resource_helper_uri(*args)
207
+ }
208
+ else
209
+ raise UriTemplateNestingError,
210
+ "Too deep nesting level (max #{MAX_NESTING_DEPTH}): #{template_uri}"
159
211
  end
160
212
  end
213
+
214
+ def nesting_depth
215
+ MethodBuilder.extract_symbols(base).size
216
+ end
161
217
  end
162
218
 
163
219
  module MethodBuilder
@@ -191,24 +247,26 @@ module Shaf
191
247
  Ruby
192
248
  end
193
249
 
194
- private
195
-
196
250
  def extract_symbols(uri)
197
251
  uri.split('/').grep(/:.*/).map { |t| t[1..-1] }.map(&:to_sym)
198
252
  end
199
253
 
254
+ def transform_symbols(uri)
255
+ uri.split('/').map do |segment|
256
+ next segment unless segment.start_with? ':'
257
+ yield segment
258
+ end.join('/')
259
+ end
260
+
261
+ private
262
+
200
263
  def interpolated_uri_string(uri)
201
- return uri if uri.split('/').empty?
202
-
203
- segments = uri.split('/').map do |segment|
204
- if segment.start_with? ':'
205
- str = segment[1..-1]
206
- "\#{#{str}.respond_to?(#{segment}) ? #{str}.#{str} : #{str}}"
207
- else
208
- segment
209
- end
264
+ return uri if uri == '/'
265
+
266
+ transform_symbols(uri) do |segment|
267
+ str = segment[1..-1]
268
+ "\#{#{str}.respond_to?(#{segment}) ? #{str}.#{str} : #{str}}"
210
269
  end
211
- segments.join('/')
212
270
  end
213
271
  end
214
272
  end