shaf 0.1.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- 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,19 @@
|
|
1
|
+
require 'shaf/generator'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module Command
|
5
|
+
class Generate < Base
|
6
|
+
|
7
|
+
identifier %r(\Ag(en(erate)?)?\Z)
|
8
|
+
usage Generator::Factory.usage.flatten.sort
|
9
|
+
|
10
|
+
def call
|
11
|
+
in_project_root do
|
12
|
+
Generator::Factory.create(*args).call
|
13
|
+
end
|
14
|
+
rescue StandardError => e
|
15
|
+
raise Command::ArgumentError, e.message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
require 'shaf/version'
|
5
|
+
|
6
|
+
module Shaf
|
7
|
+
module Command
|
8
|
+
class New < Base
|
9
|
+
|
10
|
+
identifier %r(\An(ew)?\Z)
|
11
|
+
usage 'new PROJECT_NAME'
|
12
|
+
|
13
|
+
def call
|
14
|
+
@project_name = args.first
|
15
|
+
if @project_name.nil? || @project_name.empty?
|
16
|
+
raise ArgumentError,
|
17
|
+
"Please provide a project name when using command 'new'!"
|
18
|
+
end
|
19
|
+
|
20
|
+
create_dir @project_name
|
21
|
+
Dir.chdir(@project_name) do
|
22
|
+
copy_templates
|
23
|
+
create_gemfile
|
24
|
+
create_shaf_version_file
|
25
|
+
create_ruby_version_file
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_dir(name)
|
30
|
+
return if Dir.exist? name
|
31
|
+
FileUtils.mkdir_p(name)
|
32
|
+
rescue SystemCallError
|
33
|
+
exit_with_error("Failed to create directory #{name}", 2)
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_gemfile
|
37
|
+
template_file = File.expand_path('../templates/Gemfile.erb', __FILE__)
|
38
|
+
content = File.read(template_file)
|
39
|
+
File.write "Gemfile",
|
40
|
+
ERB.new(content, 0, '%-<>').result
|
41
|
+
end
|
42
|
+
|
43
|
+
def copy_templates
|
44
|
+
template_files.each do |template|
|
45
|
+
copy_template(template)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_shaf_version_file
|
50
|
+
File.write '.shaf',
|
51
|
+
YAML.dump({'version' => Shaf::VERSION})
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_ruby_version_file
|
55
|
+
File.write '.ruby-version', RUBY_VERSION
|
56
|
+
end
|
57
|
+
|
58
|
+
def copy_template(template)
|
59
|
+
target = target_for(template)
|
60
|
+
create_dir File.dirname(target)
|
61
|
+
FileUtils.cp(template, target)
|
62
|
+
end
|
63
|
+
|
64
|
+
def template_dir
|
65
|
+
File.expand_path('../../../../templates', __FILE__)
|
66
|
+
end
|
67
|
+
|
68
|
+
def template_files
|
69
|
+
Dir["#{template_dir}/**/{*,.*}"].reject do |file|
|
70
|
+
File.directory?(file)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def target_for(template)
|
75
|
+
template.sub("#{template_dir}/", "")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# A sample Gemfile
|
3
|
+
source "https://rubygems.org"
|
4
|
+
ruby '<%= RUBY_VERSION %>'
|
5
|
+
|
6
|
+
gem 'shaf'
|
7
|
+
gem 'sinatra', require: 'sinatra/base'
|
8
|
+
gem 'rake'
|
9
|
+
gem 'thin'
|
10
|
+
gem 'sequel'
|
11
|
+
gem 'sequel'
|
12
|
+
gem 'sinatra-sequel'
|
13
|
+
gem 'bcrypt'
|
14
|
+
gem 'hal_presenter'
|
15
|
+
gem 'redcarpet'
|
16
|
+
|
17
|
+
group :production, :development do
|
18
|
+
gem 'pg'
|
19
|
+
end
|
20
|
+
|
21
|
+
group :development, :test do
|
22
|
+
gem 'sqlite3'
|
23
|
+
end
|
24
|
+
|
25
|
+
group :test do
|
26
|
+
gem 'minitest'
|
27
|
+
gem 'minitest-hooks'
|
28
|
+
gem 'rack-test'
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
class DocModel
|
5
|
+
class << self
|
6
|
+
def find(name)
|
7
|
+
@@docs ||= {}
|
8
|
+
@@docs[name] ||= load(name)
|
9
|
+
new(name) if @@docs[name]
|
10
|
+
end
|
11
|
+
|
12
|
+
def find!(name)
|
13
|
+
find(name) or raise(Errors::NotFoundError, "No documentation for #{name}")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def load(name)
|
19
|
+
file = File.join(Settings.documents_dir, "#{name}.yml")
|
20
|
+
return YAML.load(File.read(file)) if File.exist? file
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(name)
|
25
|
+
@name = name
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
return "#{@name} not found" unless @@docs[@name]
|
30
|
+
JSON.pretty_generate(@@docs[@name])
|
31
|
+
end
|
32
|
+
|
33
|
+
def attribute(attr)
|
34
|
+
attr_doc = @@docs.dig(@name, 'attributes', attr.to_s)
|
35
|
+
return attr_doc if attr_doc
|
36
|
+
raise Errors::NotFoundError,
|
37
|
+
"No documentation for #{@name} attribute '#{attr}'"
|
38
|
+
end
|
39
|
+
|
40
|
+
def link(rel)
|
41
|
+
link_doc = @@docs.dig(@name, 'links', rel.to_s)
|
42
|
+
return link_doc if link_doc
|
43
|
+
raise Errors::NotFoundError,
|
44
|
+
"No documentation for #{@name} link relation '#{rel}'"
|
45
|
+
end
|
46
|
+
|
47
|
+
def embedded(name)
|
48
|
+
embed_doc = @@docs.dig(@name, 'embeds', name.to_s)
|
49
|
+
return embed_doc if embed_doc
|
50
|
+
raise Errors::NotFoundError,
|
51
|
+
"No documentation for #{@name} embedded '#{name}'"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/shaf/errors.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
module Shaf
|
2
|
+
module Errors
|
3
|
+
class ServerError < StandardError
|
4
|
+
attr_reader :code, :title
|
5
|
+
|
6
|
+
def http_status
|
7
|
+
500
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(msg = "Unknown error", code: nil, title: nil)
|
11
|
+
super(msg)
|
12
|
+
@code = code || "UNKNOWN_ERROR"
|
13
|
+
@title = title || "Something bad happend"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BadRequestError < ServerError
|
18
|
+
def http_status
|
19
|
+
400
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(msg = nil)
|
23
|
+
msg ||= "The request cannot be understood"
|
24
|
+
super(msg, code: "INVALID_REQUEST", title: "Invalid request")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class UnauthorizedError < ServerError
|
29
|
+
def http_status
|
30
|
+
401
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(msg = nil)
|
34
|
+
msg ||= "User is not authorized"
|
35
|
+
super(msg, code: "UNAUTHORIZED", title: "Unauthorized user")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ForbiddenError < ServerError
|
40
|
+
def http_status
|
41
|
+
403
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(msg = nil)
|
45
|
+
msg ||= "User is not allowed to perform this action"
|
46
|
+
super(msg, code: "FORBIDDEN", title: "User not allowed")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class NotFoundError < ServerError
|
51
|
+
attr_reader :clazz, :id
|
52
|
+
|
53
|
+
def http_status
|
54
|
+
404
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(msg = nil, clazz: nil, id: nil)
|
58
|
+
@clazz = clazz
|
59
|
+
@id = id
|
60
|
+
msg ||= "#{clazz ? "#{clazz.to_s} r" : "R"}esource with id #{id} does not exist"
|
61
|
+
super(msg, code: "RESOURCE_NOT_FOUND", title: "Resource not found")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class UnsupportedMediaTypeError < ServerError
|
66
|
+
def http_status
|
67
|
+
415
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(msg = nil, request: nil)
|
71
|
+
content_type = request&.env["CONTENT_TYPE"]
|
72
|
+
msg = "Unsupported Media Type#{content_type ? ": #{content_type}" : ""}"
|
73
|
+
super(msg, code: "UNSUPPORTED_MEDIA_TYPE", title: "Unsupported media type")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
|
5
|
+
module Authorize
|
6
|
+
class NoPolicyError < StandardError; end
|
7
|
+
class PolicyViolationError < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :policy_class
|
10
|
+
|
11
|
+
def authorize_with(policy_class)
|
12
|
+
@policy_class = policy_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.registered(app)
|
16
|
+
app.helpers Helpers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Helpers
|
21
|
+
def policy(resource)
|
22
|
+
return @policy if @policy
|
23
|
+
user = current_user if respond_to? :current_user
|
24
|
+
@policy = self.class.policy_class&.new(user, resource)
|
25
|
+
end
|
26
|
+
|
27
|
+
def authorize(action, resource = nil)
|
28
|
+
policy(resource) or raise Authorize::NoPolicyError
|
29
|
+
@policy.public_send method_for(action)
|
30
|
+
end
|
31
|
+
|
32
|
+
def authorize!(action, resource = nil)
|
33
|
+
authorize(action, resource) or raise Authorize::PolicyViolationError
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_for(action)
|
37
|
+
return action if action.to_s.end_with? '?'
|
38
|
+
"#{action}?".to_sym
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Shaf
|
4
|
+
module ResourceUris
|
5
|
+
def resource_uris_for(*args)
|
6
|
+
CreateUriMethods.new(*args).call
|
7
|
+
include UriHelper
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_uri(name, uri)
|
11
|
+
if UriHelper.respond_to? MethodBuilder.method_name(name)
|
12
|
+
raise "resource uri #{name} can't be registered. Method already exist!"
|
13
|
+
end
|
14
|
+
method_string = MethodBuilder.as_string(name, uri)
|
15
|
+
UriHelperMethods.eval_method(method_string)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Sinatra.register ResourceUris
|
20
|
+
|
21
|
+
module UriHelperMethods
|
22
|
+
def self.register(name, &block)
|
23
|
+
define_method(name, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.eval_method(str)
|
27
|
+
class_eval str
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module UriHelper
|
32
|
+
extend UriHelperMethods
|
33
|
+
include UriHelperMethods
|
34
|
+
end
|
35
|
+
|
36
|
+
# This class register uri helper methods like:
|
37
|
+
# books_uri => /books
|
38
|
+
# book_uri(book) => /books/5
|
39
|
+
# new_book_uri => /books
|
40
|
+
# edit_book_uri(book) => /books/5/edit
|
41
|
+
#
|
42
|
+
class CreateUriMethods
|
43
|
+
def initialize(name, base: nil, plural_name: nil)
|
44
|
+
@name = name.to_s
|
45
|
+
@base = base&.sub(%r(/\Z), '') || ''
|
46
|
+
@plural_name = plural_name&.to_s || name.to_s + 's'
|
47
|
+
end
|
48
|
+
|
49
|
+
def call
|
50
|
+
if plural_name == name
|
51
|
+
register_resource_uri_by_arg
|
52
|
+
else
|
53
|
+
register_resources_uri
|
54
|
+
register_resource_uri
|
55
|
+
end
|
56
|
+
register_new_resource_uri
|
57
|
+
register_edit_resource_uri
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :name, :base, :plural_name
|
63
|
+
|
64
|
+
def register_resources_uri
|
65
|
+
uri = "#{base}/#{plural_name}"
|
66
|
+
UriHelperMethods.register "#{plural_name}_uri" do
|
67
|
+
uri.dup.freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def register_resource_uri
|
72
|
+
uri = "#{base}/#{plural_name}"
|
73
|
+
UriHelperMethods.register "#{name}_uri" do |resrc|
|
74
|
+
id = resrc.is_a?(Integer) ? resrc : resrc&.id
|
75
|
+
"#{uri}/#{id}".freeze unless id.nil?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# If a resource has the same singular and plural names, then this method
|
80
|
+
# should be used. It will return the resource uri when a resource is given
|
81
|
+
# as argument and the resources uri when no arguments are provided.
|
82
|
+
def register_resource_uri_by_arg
|
83
|
+
uri = "#{base}/#{plural_name}"
|
84
|
+
UriHelperMethods.register "#{plural_name}_uri" do |resrc = nil|
|
85
|
+
if resrc.nil?
|
86
|
+
uri.dup.freeze
|
87
|
+
else
|
88
|
+
id = resrc.is_a?(Integer) ? resrc : resrc&.id
|
89
|
+
"#{uri}/#{id}".freeze unless id.nil?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def register_new_resource_uri
|
95
|
+
uri = "#{base}/#{plural_name}/form"
|
96
|
+
UriHelperMethods.register "new_#{name}_uri" do
|
97
|
+
uri.dup.freeze
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def register_edit_resource_uri
|
102
|
+
uri = "#{base}/#{plural_name}"
|
103
|
+
UriHelperMethods.register "edit_#{name}_uri" do |resrc|
|
104
|
+
id = resrc.is_a?(Integer) ? resrc : resrc&.id
|
105
|
+
"#{uri}/#{id}/edit".freeze unless id.nil?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module MethodBuilder
|
111
|
+
class << self
|
112
|
+
def method_name(name)
|
113
|
+
"#{name}_uri"
|
114
|
+
end
|
115
|
+
|
116
|
+
def signature(name, uri)
|
117
|
+
args = extract_symbols(uri)
|
118
|
+
s = method_name(name)
|
119
|
+
s << "(#{args.join(', ')})" unless args.empty?
|
120
|
+
s
|
121
|
+
end
|
122
|
+
|
123
|
+
def as_string(name, uri)
|
124
|
+
signature = signature(name, uri)
|
125
|
+
<<~EOM
|
126
|
+
def #{signature}
|
127
|
+
\"#{interpolated_uri_string(uri)}\".freeze
|
128
|
+
end
|
129
|
+
EOM
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def extract_symbols(uri)
|
135
|
+
uri.split('/').grep(/:.*/).map { |t| t[1..-1] }.map(&:to_sym)
|
136
|
+
end
|
137
|
+
|
138
|
+
def interpolated_uri_string(uri)
|
139
|
+
return uri if uri.split('/').empty?
|
140
|
+
|
141
|
+
segments = uri.split('/').map do |segment|
|
142
|
+
if segment.start_with? ':'
|
143
|
+
str = segment[1..-1]
|
144
|
+
"\#{#{str}.respond_to?(#{segment}) ? #{str}.#{str} : #{str}}"
|
145
|
+
else
|
146
|
+
segment
|
147
|
+
end
|
148
|
+
end
|
149
|
+
segments.join('/')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|