shaf 0.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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