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