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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +1 -0
- data/bin/shaf +57 -0
- data/lib/shaf.rb +9 -0
- data/lib/shaf/api_doc.rb +124 -0
- data/lib/shaf/api_doc/comment.rb +27 -0
- data/lib/shaf/api_doc/document.rb +133 -0
- data/lib/shaf/app.rb +22 -0
- data/lib/shaf/command.rb +42 -0
- data/lib/shaf/command/console.rb +17 -0
- data/lib/shaf/command/generate.rb +19 -0
- data/lib/shaf/command/new.rb +79 -0
- data/lib/shaf/command/server.rb +15 -0
- data/lib/shaf/command/templates/Gemfile.erb +30 -0
- data/lib/shaf/doc_model.rb +54 -0
- data/lib/shaf/errors.rb +77 -0
- data/lib/shaf/extensions.rb +11 -0
- data/lib/shaf/extensions/authorize.rb +42 -0
- data/lib/shaf/extensions/resource_uris.rb +153 -0
- data/lib/shaf/formable.rb +188 -0
- data/lib/shaf/generator.rb +69 -0
- data/lib/shaf/generator/controller.rb +106 -0
- data/lib/shaf/generator/migration.rb +122 -0
- data/lib/shaf/generator/migration/add_column.rb +49 -0
- data/lib/shaf/generator/migration/create_table.rb +40 -0
- data/lib/shaf/generator/migration/drop_column.rb +45 -0
- data/lib/shaf/generator/migration/empty.rb +21 -0
- data/lib/shaf/generator/migration/rename_column.rb +48 -0
- data/lib/shaf/generator/model.rb +68 -0
- data/lib/shaf/generator/policy.rb +43 -0
- data/lib/shaf/generator/scaffold.rb +26 -0
- data/lib/shaf/generator/serializer.rb +258 -0
- data/lib/shaf/generator/templates/api/controller.rb.erb +62 -0
- data/lib/shaf/generator/templates/api/model.rb.erb +20 -0
- data/lib/shaf/generator/templates/api/policy.rb.erb +26 -0
- data/lib/shaf/generator/templates/api/serializer.rb.erb +24 -0
- data/lib/shaf/generator/templates/spec/integration_spec.rb.erb +98 -0
- data/lib/shaf/generator/templates/spec/model.rb.erb +40 -0
- data/lib/shaf/generator/templates/spec/serializer_spec.rb.erb +46 -0
- data/lib/shaf/helpers.rb +15 -0
- data/lib/shaf/helpers/json_html.rb +65 -0
- data/lib/shaf/helpers/paginate.rb +24 -0
- data/lib/shaf/helpers/payload.rb +115 -0
- data/lib/shaf/helpers/session.rb +53 -0
- data/lib/shaf/middleware.rb +1 -0
- data/lib/shaf/middleware/request_id.rb +16 -0
- data/lib/shaf/registrable_factory.rb +71 -0
- data/lib/shaf/settings.rb +33 -0
- data/lib/shaf/spec.rb +6 -0
- data/lib/shaf/spec/http_method_utils.rb +24 -0
- data/lib/shaf/spec/integration_spec.rb +53 -0
- data/lib/shaf/spec/model.rb +17 -0
- data/lib/shaf/spec/payload_test.rb +78 -0
- data/lib/shaf/spec/payload_utils.rb +176 -0
- data/lib/shaf/spec/serializer_spec.rb +24 -0
- data/lib/shaf/tasks.rb +4 -0
- data/lib/shaf/tasks/db.rb +61 -0
- data/lib/shaf/tasks/test.rb +43 -0
- data/lib/shaf/utils.rb +53 -0
- data/lib/shaf/version.rb +3 -0
- data/templates/Rakefile +13 -0
- data/templates/api/controllers/base_controller.rb +57 -0
- data/templates/api/controllers/docs_controller.rb +16 -0
- data/templates/api/controllers/root_controller.rb +8 -0
- data/templates/api/serializers/error_serializer.rb +10 -0
- data/templates/api/serializers/form_serializer.rb +42 -0
- data/templates/api/serializers/root_serializer.rb +16 -0
- data/templates/config.ru +4 -0
- data/templates/config/bootstrap.rb +12 -0
- data/templates/config/constants.rb +5 -0
- data/templates/config/customize.rb +3 -0
- data/templates/config/database.rb +40 -0
- data/templates/config/directories.rb +32 -0
- data/templates/config/helpers.rb +18 -0
- data/templates/config/initializers.rb +12 -0
- data/templates/config/initializers/db_migrations.rb +18 -0
- data/templates/config/initializers/hal_presenter.rb +6 -0
- data/templates/config/initializers/logging.rb +7 -0
- data/templates/config/initializers/sequel.rb +4 -0
- data/templates/config/settings.yml +19 -0
- data/templates/frontend/assets/css/main.css +70 -0
- data/templates/frontend/views/form.erb +16 -0
- data/templates/frontend/views/layout.erb +11 -0
- data/templates/frontend/views/payload.erb +8 -0
- data/templates/spec/integration/root_spec.rb +14 -0
- data/templates/spec/serializers/root_serializer_spec.rb +12 -0
- data/templates/spec/spec_helper.rb +4 -0
- metadata +348 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,188 @@
|
|
1
|
+
module Shaf
|
2
|
+
module Formable
|
3
|
+
class Field
|
4
|
+
attr_reader :name, :type, :value, :label
|
5
|
+
|
6
|
+
def initialize(name, params = {})
|
7
|
+
@name = name
|
8
|
+
@type = params[:type]
|
9
|
+
@label = params[:label]
|
10
|
+
@has_value = params.key? :value
|
11
|
+
@value = params[:value]
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_value?
|
15
|
+
@has_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_html
|
19
|
+
[
|
20
|
+
'<div class="form--input-group">',
|
21
|
+
label_element,
|
22
|
+
input_element,
|
23
|
+
'</div>'
|
24
|
+
].compact.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def label_element
|
30
|
+
str = (label || name || "").to_s
|
31
|
+
%Q(<label for="#{name}" class="form--label">#{str}</label>)
|
32
|
+
end
|
33
|
+
|
34
|
+
def input_element
|
35
|
+
_value = value ? %Q( value="#{value.to_s}") : ""
|
36
|
+
%Q(<input type="#{type.to_s}" class="form--input" id="#{name.to_s}" name="#{name.to_s}"#{_value}>)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Form
|
41
|
+
|
42
|
+
DEFAULT_TYPE = 'application/json'
|
43
|
+
|
44
|
+
attr_accessor :resource, :name, :title, :href, :type, :self_link
|
45
|
+
attr_reader :fields
|
46
|
+
|
47
|
+
def initialize(params = {})
|
48
|
+
@name = params[:name]
|
49
|
+
@title = params[:title]
|
50
|
+
@method = params[:method] || 'POST'
|
51
|
+
@type = params[:type] || DEFAULT_TYPE
|
52
|
+
@fields = (params[:fields] || {}).map { |name, args| Field.new(name, args) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def method=(m)
|
56
|
+
@method = m.to_s.upcase
|
57
|
+
end
|
58
|
+
|
59
|
+
def method
|
60
|
+
@method.to_s.upcase
|
61
|
+
end
|
62
|
+
|
63
|
+
def fields=(fields)
|
64
|
+
@fields = fields.map { |name, args| Field.new(name, args) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_field(name, opts)
|
68
|
+
@fields << Field.new(name, opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_html
|
72
|
+
form_element do
|
73
|
+
[
|
74
|
+
hidden_method_element,
|
75
|
+
fields.map { |f| f.to_html }.join("\n"),
|
76
|
+
submit_element,
|
77
|
+
].compact.join("\n")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def form_element
|
84
|
+
[
|
85
|
+
%Q(<form class="form" method=#{method == 'GET' ? 'GET' : 'POST'}#{href ? %Q( action="#{href.to_s}") : ''}>),
|
86
|
+
block_given? ? yield : nil,
|
87
|
+
"</form>",
|
88
|
+
].compact.join("\n")
|
89
|
+
end
|
90
|
+
|
91
|
+
def hidden_method_element
|
92
|
+
return if ['GET', 'POST'].include?(method)
|
93
|
+
%Q(<input type="hidden" name="_method" value="#{method}">)
|
94
|
+
end
|
95
|
+
|
96
|
+
def submit_element
|
97
|
+
%Q(<div class="form--input-group"><input type="submit" class="button" value="Submit"</div>)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Builder
|
102
|
+
def self.call(block)
|
103
|
+
[
|
104
|
+
new(block, create: true).call,
|
105
|
+
new(block, edit: true).call
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_reader :block
|
110
|
+
|
111
|
+
def initialize(block, create: false, edit: false)
|
112
|
+
@block = block
|
113
|
+
@create = create
|
114
|
+
@edit = edit
|
115
|
+
@form = nil
|
116
|
+
@default_method = @create ? :post : :put
|
117
|
+
end
|
118
|
+
|
119
|
+
def call
|
120
|
+
instance_exec(&block)
|
121
|
+
@form
|
122
|
+
end
|
123
|
+
|
124
|
+
def form
|
125
|
+
@form ||= Form.new(method: @default_method)
|
126
|
+
end
|
127
|
+
|
128
|
+
def name(name)
|
129
|
+
form.name = name
|
130
|
+
end
|
131
|
+
|
132
|
+
def title(title)
|
133
|
+
form.title = title
|
134
|
+
end
|
135
|
+
|
136
|
+
def method(method)
|
137
|
+
form.method = method
|
138
|
+
end
|
139
|
+
|
140
|
+
def type(type)
|
141
|
+
form.type = type
|
142
|
+
end
|
143
|
+
|
144
|
+
def fields(fields)
|
145
|
+
form.fields = fields
|
146
|
+
end
|
147
|
+
|
148
|
+
def field(name, opts = {})
|
149
|
+
form.add_field(name, opts)
|
150
|
+
end
|
151
|
+
|
152
|
+
def create(&b)
|
153
|
+
return unless @create
|
154
|
+
call_nested_block(b)
|
155
|
+
end
|
156
|
+
|
157
|
+
def edit(&b)
|
158
|
+
return unless @edit
|
159
|
+
call_nested_block(b)
|
160
|
+
end
|
161
|
+
|
162
|
+
def call_nested_block(b)
|
163
|
+
old, @block = @block, b
|
164
|
+
call
|
165
|
+
ensure
|
166
|
+
@block = old
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
module ClassMethods
|
171
|
+
attr_reader :create_form, :edit_form
|
172
|
+
|
173
|
+
def form(&block)
|
174
|
+
@create_form, @edit_form = Builder.(block)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.included(base)
|
179
|
+
base.extend(ClassMethods)
|
180
|
+
end
|
181
|
+
|
182
|
+
def edit_form
|
183
|
+
self.class.edit_form.tap do |form|
|
184
|
+
form&.resource = self
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'erb'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'shaf/registrable_factory'
|
5
|
+
|
6
|
+
module Shaf
|
7
|
+
module Generator
|
8
|
+
class Factory
|
9
|
+
extend RegistrableFactory
|
10
|
+
end
|
11
|
+
|
12
|
+
class Base
|
13
|
+
attr_reader :args
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def inherited(child)
|
17
|
+
Factory.register(child)
|
18
|
+
end
|
19
|
+
|
20
|
+
def identifier(*ids)
|
21
|
+
@identifiers = ids.flatten
|
22
|
+
end
|
23
|
+
|
24
|
+
def usage(str = nil, &block)
|
25
|
+
@usage = str || block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(*args)
|
30
|
+
@args = args.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
def template_dir
|
34
|
+
File.expand_path('../generator/templates', __FILE__)
|
35
|
+
end
|
36
|
+
|
37
|
+
def read_template(file, directory = nil)
|
38
|
+
directory ||= template_dir
|
39
|
+
filename = File.join(directory, file)
|
40
|
+
filename << ".erb" unless filename.end_with?(".erb")
|
41
|
+
File.read(filename)
|
42
|
+
end
|
43
|
+
|
44
|
+
def render(template, locals = {})
|
45
|
+
str = read_template(template)
|
46
|
+
locals[:changes] ||= []
|
47
|
+
b = OpenStruct.new(locals).instance_eval { binding }
|
48
|
+
ERB.new(str, 0, '%-<>').result(b)
|
49
|
+
rescue SystemCallError => e
|
50
|
+
puts "Failed to render template #{template}: #{e.message}"
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_output(file, content)
|
55
|
+
dir = File.dirname(file)
|
56
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
57
|
+
File.write(file, content)
|
58
|
+
puts "Added: #{file}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'shaf/generator/controller'
|
65
|
+
require 'shaf/generator/migration'
|
66
|
+
require 'shaf/generator/model'
|
67
|
+
require 'shaf/generator/policy'
|
68
|
+
require 'shaf/generator/scaffold'
|
69
|
+
require 'shaf/generator/serializer'
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Shaf
|
2
|
+
module Generator
|
3
|
+
class Controller < Base
|
4
|
+
|
5
|
+
identifier :controller
|
6
|
+
usage 'generate controller RESOURCE_NAME [attribute:type] [..]'
|
7
|
+
|
8
|
+
def call
|
9
|
+
if name.empty?
|
10
|
+
raise Command::ArgumentError,
|
11
|
+
"Please provide a controller name when using the controller generator!"
|
12
|
+
end
|
13
|
+
|
14
|
+
create_controller
|
15
|
+
create_integration_spec
|
16
|
+
add_link_to_root
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
args.first || ""
|
21
|
+
end
|
22
|
+
|
23
|
+
def params
|
24
|
+
args[1..-1].map { |param| param.split(':')}
|
25
|
+
end
|
26
|
+
|
27
|
+
def plural_name
|
28
|
+
Utils::pluralize(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def template
|
32
|
+
'api/controller.rb'
|
33
|
+
end
|
34
|
+
|
35
|
+
def spec_template
|
36
|
+
'spec/integration_spec.rb'
|
37
|
+
end
|
38
|
+
|
39
|
+
def target
|
40
|
+
"api/controllers/#{plural_name}_controller.rb"
|
41
|
+
end
|
42
|
+
|
43
|
+
def spec_target
|
44
|
+
"spec/integration/#{plural_name}_controller_spec.rb"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_controller
|
48
|
+
content = render(template, opts)
|
49
|
+
write_output(target, content)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_integration_spec
|
53
|
+
content = render(spec_template, opts)
|
54
|
+
write_output(spec_target, content)
|
55
|
+
end
|
56
|
+
|
57
|
+
def opts
|
58
|
+
{
|
59
|
+
name: name,
|
60
|
+
plural_name: plural_name,
|
61
|
+
serializer_class_name: "#{name.capitalize}Serializer",
|
62
|
+
model_class_name: name.capitalize,
|
63
|
+
controller_class_name: "#{plural_name.capitalize}Controller",
|
64
|
+
policy_class_name: "#{name.capitalize}Policy",
|
65
|
+
policy_file: "policies/#{name}_policy",
|
66
|
+
params: params
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_link_to_root
|
71
|
+
file = "api/serializers/root_serializer.rb"
|
72
|
+
unless File.exist? file
|
73
|
+
puts "Warning: file '#{file}' does not exist. "\
|
74
|
+
"Not adding any link to the #{plural_name} collection"
|
75
|
+
end
|
76
|
+
added = false
|
77
|
+
content = []
|
78
|
+
File.readlines(file).reverse.each do |line|
|
79
|
+
if match = !added && line.match(/^(\s*)link /)
|
80
|
+
content.unshift link_content("#{match[1]}")
|
81
|
+
added = true
|
82
|
+
end
|
83
|
+
content.unshift(line)
|
84
|
+
end
|
85
|
+
File.open(file, 'w') { |f| f.puts content }
|
86
|
+
puts "Modified: #{file}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def link_content(indentation = "")
|
90
|
+
<<~EOS.split("\n").map { |line| "#{indentation}#{line}" }
|
91
|
+
|
92
|
+
# Auto generated doc:
|
93
|
+
# Link to the collection of #{plural_name}.
|
94
|
+
# Method: GET
|
95
|
+
# Example:
|
96
|
+
# ```
|
97
|
+
# curl -H "Accept: application/json" \\
|
98
|
+
# -H "Authorization: abcdef" \\
|
99
|
+
# /#{plural_name}/5
|
100
|
+
#```
|
101
|
+
link :#{plural_name}, #{plural_name}_uri
|
102
|
+
EOS
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Generator
|
5
|
+
module Migration
|
6
|
+
|
7
|
+
class Factory
|
8
|
+
extend RegistrableFactory
|
9
|
+
end
|
10
|
+
|
11
|
+
class Generator < Generator::Base
|
12
|
+
identifier :migration
|
13
|
+
usage { Factory.usage }
|
14
|
+
|
15
|
+
def call
|
16
|
+
generator = args.empty? ? Empty.new : Factory.create(*args)
|
17
|
+
(target, content) = generator.call
|
18
|
+
write_output(target, content)
|
19
|
+
rescue StandardError => e
|
20
|
+
raise Command::ArgumentError, e.message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Base
|
25
|
+
DB_COL_TYPES = {
|
26
|
+
integer: ['Integer :%s', ':%s, Integer'],
|
27
|
+
varchar: ['String %s', ':%s, String'],
|
28
|
+
string: ['String :%s', ':%s, String'],
|
29
|
+
text: ['String :%s, text: true', ':%s, String, text: true'],
|
30
|
+
blob: ['File :%s', ':%s, File'],
|
31
|
+
bigint: ['Bignum :%s', ':%s, Bignum'],
|
32
|
+
double: ['Float :%s', ':%s, Float'],
|
33
|
+
numeric: ['BigDecimal :%s', ':%s, BigDecimal'],
|
34
|
+
date: ['Date :%s', ':%s, Date'],
|
35
|
+
timestamp: ['DateTime :%s', ':%s, DateTime'],
|
36
|
+
time: ['Time :%s', ':%s, Time'],
|
37
|
+
bool: ['TrueClass :%s', ':%s, TrueClass'],
|
38
|
+
boolean: ['TrueClass :%s', ':%s, TrueClass'],
|
39
|
+
}
|
40
|
+
|
41
|
+
attr_reader :args
|
42
|
+
|
43
|
+
class << self
|
44
|
+
def inherited(child)
|
45
|
+
Factory.register(child)
|
46
|
+
end
|
47
|
+
|
48
|
+
def identifier(*ids)
|
49
|
+
@identifiers = ids.flatten.map(&:to_s)
|
50
|
+
end
|
51
|
+
|
52
|
+
def usage(str = nil, &block)
|
53
|
+
@usage = str || block
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(*args)
|
58
|
+
@args = args.dup
|
59
|
+
end
|
60
|
+
|
61
|
+
def call
|
62
|
+
validate_args
|
63
|
+
name = compile_migration_name
|
64
|
+
compile_changes
|
65
|
+
[target(name), render]
|
66
|
+
rescue StandardError => e
|
67
|
+
raise Command::ArgumentError, e.message
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_change(change)
|
71
|
+
@changes ||= []
|
72
|
+
@changes << change if change
|
73
|
+
end
|
74
|
+
|
75
|
+
def db_type(type)
|
76
|
+
type ||= :string
|
77
|
+
DB_COL_TYPES[type.to_sym] or raise "Column type '#{type}' not supported"
|
78
|
+
end
|
79
|
+
|
80
|
+
def column_def(str, create: true)
|
81
|
+
name, type = str.split(':')
|
82
|
+
format db_type(type)[create ? 0 : 1], name.downcase
|
83
|
+
end
|
84
|
+
|
85
|
+
def target(name)
|
86
|
+
raise "Migration filename is nil" unless name
|
87
|
+
"db/migrations/#{timestamp}_#{name}.rb"
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def timestamp
|
93
|
+
DateTime.now.strftime("%Y%m%d%H%M%S")
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_timestamp_columns?
|
97
|
+
if File.exist? 'config/initializers/sequel.rb'
|
98
|
+
require 'config/initializers/sequel'
|
99
|
+
Sequel::Model.plugins.include? Sequel::Plugins::Timestamps
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def render
|
104
|
+
<<~EOS
|
105
|
+
Sequel.migration do
|
106
|
+
change do
|
107
|
+
#{@changes.flatten.join("\n ")}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
EOS
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
require 'shaf/generator/migration/add_column'
|
119
|
+
require 'shaf/generator/migration/create_table'
|
120
|
+
require 'shaf/generator/migration/drop_column'
|
121
|
+
require 'shaf/generator/migration/empty'
|
122
|
+
require 'shaf/generator/migration/rename_column'
|