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.
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,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,15 @@
1
+ module Shaf
2
+ module Command
3
+ class Server < Base
4
+
5
+ identifier %r(\As(erver)?\Z)
6
+ usage 'server'
7
+
8
+ def call
9
+ bootstrap
10
+ App.instance.run!
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -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
@@ -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,11 @@
1
+ require 'shaf/extensions/resource_uris'
2
+ require 'shaf/extensions/authorize'
3
+
4
+ module Shaf
5
+ def self.extensions
6
+ [
7
+ ResourceUris,
8
+ Authorize,
9
+ ]
10
+ end
11
+ 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