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,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
|