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,62 @@
1
+ require '<%= policy_file %>'
2
+
3
+ class <%= controller_class_name %> < BaseController
4
+
5
+ resource_uris_for :<%= name %>
6
+
7
+ authorize_with <%= policy_class_name %>
8
+
9
+ get '/<%= plural_name %>/form' do
10
+ form = <%= model_class_name %>.create_form
11
+ form.self_link = new_<%= name %>_uri
12
+ form.href = <%= plural_name %>_uri
13
+ respond_with form
14
+ end
15
+
16
+ get '/<%= plural_name %>/:id/edit' do
17
+ form = <%= name %>.edit_form
18
+ form.self_link = edit_<%= name %>_uri(<%= name %>)
19
+ form.href = <%= name %>_uri(<%= name %>)
20
+ respond_with form
21
+ end
22
+
23
+ get '/<%= plural_name %>/:id' do
24
+ respond_with <%= name %>
25
+ end
26
+
27
+ put '/<%= plural_name %>/:id' do
28
+ authorize! :write
29
+ <%= name %>.update(<%= name %>_params)
30
+ respond_with <%= name %>
31
+ end
32
+
33
+ delete '/<%= plural_name %>/:id' do
34
+ authorize! :write
35
+ <%= name %>.destroy
36
+ status 204
37
+ end
38
+
39
+ get '/<%= plural_name %>' do
40
+ collection = paginate(<%= model_class_name %>.order(:created_at).reverse)
41
+ respond_with_collection collection, serializer: <%= serializer_class_name %>
42
+ end
43
+
44
+ post '/<%= plural_name %>' do
45
+ authorize! :write
46
+ <%= name %> = <%= model_class_name %>.create(<%= name %>_params)
47
+ headers({ "Location" => <%= name %>_uri(<%= name %>) })
48
+ respond_with <%= name %>, status: 201
49
+ end
50
+
51
+ def <%= name %>_params
52
+ # Generated method - TODO: Remove any params that should not be allowed!
53
+ safe_params(<%= params.map { |p| ":#{p[0]}" }.join(', ') %>)
54
+ end
55
+
56
+ def <%= name %>
57
+ <%= model_class_name %>[params['id']].tap do |<%= name %>|
58
+ raise NotFoundError.new(clazz: <%= model_class_name %>, id: params['id']) unless <%= name %>
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,20 @@
1
+ class <%= class_name %> < Sequel::Model
2
+ include Shaf::Formable
3
+
4
+ <% if form_fields.any? %>
5
+ form do
6
+ <%= form_fields.join("\n ") %>
7
+
8
+ create do
9
+ title 'Create <%= class_name %>'
10
+ name 'create-<%= model_name %>'
11
+ end
12
+
13
+ edit do
14
+ title 'Update <%= class_name %>'
15
+ name 'update-<%= model_name %>'
16
+ end
17
+ end
18
+ <% end %>
19
+ end
20
+
@@ -0,0 +1,26 @@
1
+ class <%= policy_class_name %>
2
+ include HALPresenter::Policy::DSL
3
+
4
+ # Auto generated policy: Update this file to suite your API!
5
+
6
+ <%= attributes.join("\n ") %>
7
+
8
+ link :up
9
+
10
+ link :edit, :'create-form', :'edit-form', :delete do
11
+ write?
12
+ end
13
+
14
+ embed :'create-form', :'edit-form' do
15
+ write?
16
+ end
17
+
18
+ def read?
19
+ true
20
+ end
21
+
22
+ def write?
23
+ # !!current_user
24
+ true
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ require 'policies/<%= policy_name %>'
2
+
3
+ class <%= class_name %>
4
+ extend HALPresenter
5
+ extend Shaf::UriHelper
6
+
7
+ model <%= model_class_name %>
8
+ policy <%= policy_class_name %>
9
+ <% attributes_with_doc.each do |attr| %>
10
+ <%= attr.join("\n ") %>
11
+ <% end %>
12
+ <% curies_with_doc.each do |curie| %>
13
+ <%= curie.join("\n ") %>
14
+ <% end %>
15
+ <% links_with_doc.each do |link| %>
16
+ <%= link.join("\n ") %>
17
+ <% end %>
18
+ <% embeds_with_doc.each do |embed| %>
19
+ <%= embed.join("\n ") %>
20
+ <% end %>
21
+ <% if collection_with_doc %>
22
+ <%= collection_with_doc.join("\n ") %>
23
+ <% end %>
24
+ end
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe <%= model_class_name %>, type: :integration do
4
+
5
+ it "returns a <%= name %>" do
6
+ <%= name %> = <%= model_class_name %>.create
7
+ get <%= name %>_uri(<%= name %>)
8
+ status.must_equal 200
9
+ link_rels.must_include(:self)
10
+ links[:self][:href].must_equal <%= name %>_uri(<%= name %>)
11
+ end
12
+
13
+ it "lists all <%= plural_name %>" do
14
+ <%= model_class_name %>.create
15
+ <%= model_class_name %>.create
16
+
17
+ get <%= plural_name %>_uri
18
+ status.must_equal 200
19
+ link_rels.must_include(:self)
20
+ links[:self][:href].must_include <%= plural_name %>_uri
21
+ embedded :'<%= plural_name %>' do
22
+ <%= plural_name %> = last_payload
23
+ <%= plural_name %>.size.must_equal 2
24
+ end
25
+ end
26
+
27
+ it "can create <%= plural_name %>" do
28
+ get <%= plural_name %>_uri
29
+
30
+ embedded :'doc:create-form' do
31
+ links[:self][:href].must_equal new_<%= name %>_uri
32
+ attributes[:href].must_equal <%= plural_name %>_uri
33
+ attributes[:method].must_equal "POST"
34
+ attributes[:name].must_equal "create-<%= name %>"
35
+ attributes[:title].must_equal "Create <%= model_class_name %>"
36
+ attributes[:type].must_equal "application/json"
37
+ attributes[:fields].size.must_equal <%= params.size %>
38
+
39
+ payload = fill_form attributes[:fields]
40
+ post attributes[:href], payload
41
+ status.must_equal 201
42
+ link_rels.must_include(:self)
43
+ headers["Location"].must_equal links[:self][:href]
44
+ end
45
+
46
+ get <%= plural_name %>_uri
47
+ status.must_equal 200
48
+ links[:self][:href].must_include <%= plural_name %>_uri
49
+ embedded(:'<%= plural_name %>').size.must_equal 1
50
+
51
+ embedded :'<%= plural_name %>' do
52
+ <%= name %> = last_payload.first
53
+ <% params.each do |param| -%>
54
+ <% if param[1] == 'string' -%>
55
+ <%= name %>[:<%= param[0] %>].must_equal "value for <%= param[0] %>"
56
+ <% elsif param[1] == 'integer' -%>
57
+ <%= name %>[:<%= param[0] %>].must_equal "<%= param[0] %>".size
58
+ <% end -%>
59
+ <% end -%>
60
+ end
61
+ end
62
+
63
+ it "<%= plural_name %> can be updated" do
64
+ <%= name %> = <%= model_class_name %>.create
65
+ get <%= name %>_uri(<%= name %>)
66
+ status.must_equal 200
67
+ link_rels.must_include(:'doc:edit-form')
68
+
69
+ embedded :'doc:edit-form' do
70
+ links[:self][:href].must_equal edit_<%= name %>_uri(<%= name %>)
71
+ attributes[:href].must_equal <%= name %>_uri(<%= name %>)
72
+ attributes[:method].must_equal "PUT"
73
+ attributes[:name].must_equal "update-<%= name %>"
74
+ attributes[:title].must_equal "Update <%= model_class_name %>"
75
+ attributes[:type].must_equal "application/json"
76
+ attributes[:fields].size.must_equal <%= params.size %>
77
+
78
+ payload = fill_form attributes[:fields]
79
+ put attributes[:href], payload
80
+ status.must_equal 200
81
+ link_rels.must_include(:self)
82
+ end
83
+ end
84
+
85
+ it "<%= plural_name %> can be deleted" do
86
+ <%= name %> = <%= model_class_name %>.create
87
+ get <%= name %>_uri(<%= name %>)
88
+ status.must_equal 200
89
+ link_rels.must_include(:'doc:delete')
90
+
91
+ follow_rel(:'doc:delete', method: :delete)
92
+ status.must_equal 204
93
+
94
+ get <%= name %>_uri(<%= name %>)
95
+ status.must_equal 404
96
+ end
97
+
98
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ module Model
4
+ class UserTest < TestCase
5
+ def setup
6
+ @user = User.create(
7
+ username: 'test_user',
8
+ password: 'hidden',
9
+ email: 'test@user.com',
10
+ )
11
+ end
12
+
13
+ def test_create
14
+ assert @user
15
+ assert_equal @user.username, 'test_user'
16
+ assert_equal @user.email, 'test@user.com'
17
+ created = @user.created_at
18
+ assert (created..(created + 30)).include? Time.now
19
+ end
20
+
21
+ def test_update_user
22
+ digest = @user.password_digest
23
+ @user.update(
24
+ username: 'test_User',
25
+ email: 'test@User.com',
26
+ password: 'hidden2'
27
+ )
28
+ assert @user.created_at < @user.updated_at
29
+ assert_equal 'test_User', @user.username
30
+ assert_equal 'test@User.com', @user.email
31
+ refute_equal digest, @user.password_digest
32
+ end
33
+
34
+ def test_delete_user
35
+ @user.destroy
36
+ assert_nil User.where(username: @user.username).first
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe <%= class_name %> do
4
+
5
+ let(:resource) do
6
+ <%= model_class_name %>.new.
7
+ tap { |<%= name %>| <%= name %>.id = 5 }
8
+ end
9
+
10
+ describe "when current_user is nil" do
11
+ before do
12
+ set_payload HALPresenter.to_hal(resource)
13
+ end
14
+
15
+ it "serializes attributes" do
16
+ <% attributes.each do |attr| -%>
17
+ attributes.keys.must_include(<%= attr %>)
18
+ <% end -%>
19
+ end
20
+
21
+ it "serializes links" do
22
+ <% ['self', 'doc:up'].each do |rel| -%>
23
+ link_rels.must_include(:'<%= rel %>')
24
+ <% end -%>
25
+ end
26
+ end
27
+
28
+ describe "when current_user is present" do
29
+ before do
30
+ set_payload HALPresenter.to_hal(resource, current_user: "Bengt")
31
+ end
32
+
33
+ it "serializes attributes" do
34
+ <% attributes.each do |attr| -%>
35
+ attributes.keys.must_include(<%= attr %>)
36
+ <% end -%>
37
+ end
38
+
39
+ it "serializes links" do
40
+ <% links.each do |rel| -%>
41
+ link_rels.must_include(:'<%= rel %>')
42
+ <% end -%>
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,15 @@
1
+ require 'shaf/helpers/payload'
2
+ require 'shaf/helpers/json_html'
3
+ require 'shaf/helpers/paginate'
4
+ require 'shaf/helpers/session'
5
+
6
+ module Shaf
7
+ def self.helpers
8
+ [
9
+ Payload,
10
+ JsonHtml,
11
+ Paginate,
12
+ Session,
13
+ ]
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ module Shaf
2
+ module JsonHtml
3
+
4
+ def json2html(json)
5
+ o = JSON.parse(json)
6
+ "<pre><code>#{to_html(o)}</code></pre>"
7
+ end
8
+
9
+ private
10
+
11
+ def to_html(obj, indent: 0, pre_indent: "")
12
+ case obj
13
+ when Array
14
+ html_array(obj, indent, pre_indent)
15
+ when Hash
16
+ html_hash(obj, indent, pre_indent)
17
+ else
18
+ html_scalar(obj, indent, pre_indent)
19
+ end
20
+ end
21
+
22
+ def html_array(a, indent, pre_indent)
23
+ array_of_strings = a.map do |e|
24
+ to_html(e, indent: indent + 1, pre_indent: indentation(indent + 1))
25
+ end
26
+
27
+ <<~EOS.chomp
28
+ #{pre_indent}<span>[</span>
29
+ #{array_of_strings.join(",\n")}
30
+ #{indentation(indent)}<span>]</span>
31
+ EOS
32
+ end
33
+
34
+ def html_hash(h, indent, pre_indent)
35
+ <<~EOS.chomp
36
+ #{pre_indent}<span>{</span>
37
+ #{h.map { |k,v| sub_hash(k,v, indent + 1) }.join(",\n")},
38
+ #{indentation(indent)}<span>}</span>
39
+ EOS
40
+ end
41
+
42
+ def html_scalar(s, indent, pre_indent)
43
+ q = around(s)
44
+ format "%s%s%s%s", pre_indent, q, s, q
45
+ end
46
+
47
+ def sub_hash(key, value, indent)
48
+ if key == 'href'
49
+ %Q(#{indentation(indent)}"#{key}"<span>:</span> <a href="#{value}">#{value}</a>)
50
+ else
51
+ "#{indentation(indent)}\"#{key}\"<span>:</span> #{to_html(value, indent: indent) }"
52
+ end
53
+ end
54
+
55
+ def indentation(i)
56
+ " " * i
57
+ end
58
+
59
+ def around(obj)
60
+ return '"' if obj.is_a? String
61
+ ""
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ module Shaf
2
+ module Paginate
3
+
4
+ def current_page
5
+ page = (params[:page] || 1).to_i
6
+ page == 0 ? 1 : page
7
+ end
8
+
9
+ def paginate!(collection, per_page = PAGINATION_PER_PAGE)
10
+ unless collection.respond_to? :paginate
11
+ log.warning "Trying to paginate a collection that doesn't " \
12
+ "support pagination: #{collection}"
13
+ return
14
+ end
15
+
16
+ per_page = params[:per_page].to_i if params[:per_page]
17
+ collection.paginate(current_page, per_page)
18
+ end
19
+
20
+ def paginate(collection, per_page = PAGINATION_PER_PAGE)
21
+ paginate!(collection.dup, per_page)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,115 @@
1
+ module Shaf
2
+ module Payload
3
+ def supported_response_types(resource)
4
+ [
5
+ mime_type(:hal),
6
+ mime_type(:json),
7
+ mime_type(:html)
8
+ ]
9
+ end
10
+
11
+ def preferred_response_type(resource)
12
+ supported_types = supported_response_types(resource)
13
+ request.preferred_type(supported_types)
14
+ end
15
+
16
+ def prefer_html?
17
+ request.preferred_type.to_s == mime_type(:html)
18
+ end
19
+
20
+ private
21
+
22
+ def payload
23
+ @@request_id ||= nil
24
+ @@payload ||= nil
25
+
26
+ if @@request_id != request.env["REQUEST_ID"]
27
+ @@request_id = request.env["REQUEST_ID"]
28
+ @@payload = parse_payload
29
+ end
30
+ @@payload
31
+ end
32
+
33
+ def read_input
34
+ request.body.rewind unless request.body.pos == 0
35
+ request.body.read
36
+ ensure
37
+ request.body.rewind
38
+ end
39
+
40
+ def parse_payload
41
+ if request.env['CONTENT_TYPE'] == 'application/x-www-form-urlencoded'
42
+ return params.reject { |key,_| ['captures', 'splat'].include? key }
43
+ end
44
+
45
+ input = read_input
46
+ return {} if input.empty?
47
+
48
+ if request.env['CONTENT_TYPE'] =~ %r(\Aapplication/json)
49
+ JSON.parse(input)
50
+ else
51
+ raise ::UnsupportedMediaTypeError.new(request: request)
52
+ end
53
+ rescue StandardError
54
+ raise ::BadRequestError.new
55
+ end
56
+
57
+ def safe_params(*fields)
58
+ return {} unless payload
59
+ {}.tap do |allowed|
60
+ fields.each do |field|
61
+ f = field.to_s.downcase
62
+ allowed[f.to_sym] = payload[f] if payload[f]
63
+ end
64
+ end
65
+ end
66
+
67
+ def ignore_form_input?(name)
68
+ return name == '_method'
69
+ end
70
+
71
+ def respond_with_collection(resource, status: 200, serializer: nil)
72
+ respond_with(resource, status: status, serializer: serializer, collection: true)
73
+ end
74
+
75
+ def respond_with(resource, status: 200, serializer: nil, collection: false)
76
+ status(status)
77
+
78
+ preferred_response = preferred_response_type(resource)
79
+ serialized = serialize(resource, serializer, collection)
80
+
81
+ if preferred_response == mime_type(:html)
82
+ respond_with_html(resource, serialized)
83
+ else
84
+ respond_with_hal(resource, serialized)
85
+ end
86
+ end
87
+
88
+ def serialize(resource, serializer, collection)
89
+ serializer ||= HALPresenter
90
+ if collection
91
+ serializer.to_collection(resource, current_user: current_user)
92
+ else
93
+ serializer.to_hal(resource, current_user: current_user)
94
+ end
95
+ end
96
+
97
+ def respond_with_hal(resource, serialized)
98
+ log.debug "Response payload (#{resource.class}): #{serialized}"
99
+ content_type :hal
100
+ body serialized
101
+ end
102
+
103
+ def respond_with_html(resource, serialized)
104
+ log.debug "Responding with html. Output payload (#{resource.class}): #{serialized}"
105
+ content_type :html
106
+ case resource
107
+ when Shaf::Formable::Form
108
+ body erb(:form, locals: {form: resource, serialized: serialized})
109
+ else
110
+ body erb(:payload, locals: {serialized: serialized})
111
+ end
112
+ end
113
+
114
+ end
115
+ end