katapult 0.1.0

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +11 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +122 -0
  9. data/Rakefile +14 -0
  10. data/bin/katapult +44 -0
  11. data/features/binary.feature +48 -0
  12. data/features/configuration.feature +24 -0
  13. data/features/katapult.feature +201 -0
  14. data/features/model.feature +203 -0
  15. data/features/navigation.feature +80 -0
  16. data/features/step_definitions/db_steps.rb +8 -0
  17. data/features/step_definitions/file_steps.rb +14 -0
  18. data/features/step_definitions/katapult_steps.rb +14 -0
  19. data/features/step_definitions/rails_steps.rb +44 -0
  20. data/features/step_definitions/test_steps.rb +7 -0
  21. data/features/support/env.rb +16 -0
  22. data/features/wui.feature +319 -0
  23. data/katapult.gemspec +35 -0
  24. data/katapult.png +0 -0
  25. data/lib/generators/katapult/basics/basics_generator.rb +95 -0
  26. data/lib/generators/katapult/basics/templates/Gemfile +76 -0
  27. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application.css.sass +6 -0
  28. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_all.css.sass +4 -0
  29. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_items.css.sass +11 -0
  30. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_layout.css.sass +26 -0
  31. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_navigation.css.sass +11 -0
  32. data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_tools.css.sass +12 -0
  33. data/lib/generators/katapult/basics/templates/config/database.sample.yml +16 -0
  34. data/lib/generators/katapult/basics/templates/config/database.yml +13 -0
  35. data/lib/generators/katapult/basics/templates/config/spring.rb +3 -0
  36. data/lib/generators/katapult/basics/templates/features/support/env-custom.rb +3 -0
  37. data/lib/generators/katapult/basics/templates/features/support/paths.rb +47 -0
  38. data/lib/generators/katapult/cucumber_features/cucumber_features_generator.rb +23 -0
  39. data/lib/generators/katapult/cucumber_features/templates/feature.feature +59 -0
  40. data/lib/generators/katapult/haml/haml_generator.rb +90 -0
  41. data/lib/generators/katapult/haml/templates/_form.html.haml +38 -0
  42. data/lib/generators/katapult/haml/templates/app/views/layouts/application.html.haml +25 -0
  43. data/lib/generators/katapult/haml/templates/custom_action.html.haml +5 -0
  44. data/lib/generators/katapult/haml/templates/edit.html.haml +4 -0
  45. data/lib/generators/katapult/haml/templates/index.html.haml +29 -0
  46. data/lib/generators/katapult/haml/templates/new.html.haml +4 -0
  47. data/lib/generators/katapult/haml/templates/show.html.haml +41 -0
  48. data/lib/generators/katapult/install/install_generator.rb +14 -0
  49. data/lib/generators/katapult/install/templates/lib/katapult/application_model.rb +18 -0
  50. data/lib/generators/katapult/model/model_generator.rb +59 -0
  51. data/lib/generators/katapult/model/templates/app/models/shared/does_flag.rb +32 -0
  52. data/lib/generators/katapult/model/templates/model.rb +21 -0
  53. data/lib/generators/katapult/model_specs/model_specs_generator.rb +51 -0
  54. data/lib/generators/katapult/model_specs/templates/model_spec.rb +34 -0
  55. data/lib/generators/katapult/navigation/navigation_generator.rb +25 -0
  56. data/lib/generators/katapult/navigation/templates/app/models/navigation.rb +12 -0
  57. data/lib/generators/katapult/transform/transform_generator.rb +47 -0
  58. data/lib/generators/katapult/w_u_i/templates/_route.rb +13 -0
  59. data/lib/generators/katapult/w_u_i/templates/controller.rb +106 -0
  60. data/lib/generators/katapult/w_u_i/w_u_i_generator.rb +57 -0
  61. data/lib/katapult.rb +5 -0
  62. data/lib/katapult/action.rb +44 -0
  63. data/lib/katapult/application_model.rb +45 -0
  64. data/lib/katapult/attribute.rb +83 -0
  65. data/lib/katapult/element.rb +72 -0
  66. data/lib/katapult/generator.rb +28 -0
  67. data/lib/katapult/model.rb +33 -0
  68. data/lib/katapult/navigation.rb +22 -0
  69. data/lib/katapult/parser.rb +39 -0
  70. data/lib/katapult/util.rb +16 -0
  71. data/lib/katapult/version.rb +3 -0
  72. data/lib/katapult/wui.rb +77 -0
  73. data/script/console +16 -0
  74. data/spec/action_spec.rb +44 -0
  75. data/spec/attribute_spec.rb +48 -0
  76. data/spec/model_spec.rb +18 -0
  77. data/spec/spec_helper.rb +19 -0
  78. data/spec/util_spec.rb +23 -0
  79. data/spec/wui_spec.rb +49 -0
  80. metadata +253 -0
@@ -0,0 +1,34 @@
1
+ require 'rails_helper'
2
+
3
+ describe <%= model.name :class %> do
4
+
5
+ describe '#to_s' do
6
+ <%- if model.label_attr -%>
7
+ it 'returns the #<%= model.label_attr.name %> attribute' do
8
+ subject.<%= model.label_attr.name %> = <%= model.label_attr.test_value.inspect %>
9
+ expect(subject.to_s).to eql(<%= model.label_attr.test_value.to_s.inspect %>)
10
+ end
11
+ <%- else -%>
12
+ it 'returns its class name with its id' do
13
+ subject.id = 17
14
+ expect(subject.to_s).to eql("<%= model.name %>#17")
15
+ end
16
+ <%- end -%>
17
+ end
18
+ <% specable_attrs.each do |attr| -%>
19
+
20
+ describe '#<%= attr.name %>' do
21
+ <%- if attr.assignable_values.present? -%>
22
+ it { is_expected.to allow_value(<%= assignable_value_for(attr).inspect %>).for(:<%= attr.name %>) }
23
+ it { is_expected.to_not allow_value(<%= unassignable_value_for(attr).inspect %>).for(:<%= attr.name %>) }
24
+ <%- end -%>
25
+ <%- unless attr.default.nil? -%>
26
+
27
+ it 'has a default' do
28
+ expect( subject.<%= attr.name %> ).to eql(<%= attr.default.inspect %>)
29
+ end
30
+ <%- end -%>
31
+ end
32
+ <% end -%>
33
+
34
+ end
@@ -0,0 +1,25 @@
1
+ # Generate navigation.
2
+
3
+ require 'katapult/generator'
4
+
5
+ module Katapult
6
+ module Generators
7
+ class NavigationGenerator < Katapult::Generator
8
+
9
+ desc 'Generate the navigation'
10
+ source_root File.expand_path('../templates', __FILE__)
11
+
12
+
13
+ def create_navigation
14
+ template 'app/models/navigation.rb'
15
+ end
16
+
17
+ private
18
+
19
+ def navigation
20
+ @element
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ class Navigation
2
+ include Navy::Description
3
+
4
+ navigation :<%= name.downcase %> do
5
+ <% navigation.wuis.each do |wui| -%>
6
+ <%- if wui.find_action(:index) -%>
7
+ section <%= navigation.section_name(wui) %>, <%= wui.model_name(:human_plural).titlecase.inspect %>, <%= wui.path(:index) %>
8
+ <%- end -%>
9
+ <% end -%>
10
+ end
11
+
12
+ end
@@ -0,0 +1,47 @@
1
+ # Generate files and directories from an application model file. Afterwards do
2
+ # any setup left necessary (e.g. updating the database).
3
+
4
+ # The application model transformation is split into two parts:
5
+ # 1) parse the model into an object-based representation
6
+ # 2) render the parsed model into code
7
+
8
+ require 'katapult/parser'
9
+
10
+ module Katapult
11
+ class TransformGenerator < Rails::Generators::Base
12
+ desc 'Transform the katapult application model'
13
+
14
+ argument :path, required: true, type: :string,
15
+ description: 'The path to the application model file'
16
+
17
+
18
+ def transform_application_model
19
+ say_status :parse, path
20
+ @app_model = Katapult::Parser.new.parse(path)
21
+
22
+ say_status :render, "into #{application_name}"
23
+ @app_model.render
24
+ end
25
+
26
+ def write_root_route
27
+ root_wui = @app_model.wuis.find do |wui|
28
+ wui.find_action :index
29
+ end
30
+
31
+ route "root '#{ root_wui.model_name(:variables) }#index'" if root_wui
32
+ end
33
+
34
+ def migrate
35
+ run 'bin/rake db:drop:all &> /dev/null'
36
+ run 'bin/rake db:create db:migrate RAILS_ENV=development'
37
+ run 'bin/rake db:create db:migrate RAILS_ENV=test'
38
+ end
39
+
40
+ private
41
+
42
+ def application_name
43
+ File.basename(Dir.pwd)
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ % rails_actions = wui.actions - wui.custom_actions
2
+ resources <%= model_name(:symbols) %>, only: <%= rails_actions.map(&:name).map(&:to_sym) %> do
3
+ member do
4
+ % wui.custom_actions.select(&:member?).each do |action|
5
+ <%= action.method %> '<%= action.name %>'
6
+ % end
7
+ end
8
+ collection do
9
+ % wui.custom_actions.select(&:collection?).each do |action|
10
+ <%= action.method %> '<%= action.name %>'
11
+ % end
12
+ end
13
+ end
@@ -0,0 +1,106 @@
1
+ class <%= model_name(:classes) %>Controller < ApplicationController
2
+ <%- if navigation -%>
3
+
4
+ before_filter :set_section
5
+ <%- end -%>
6
+ <% if wui.find_action :index -%>
7
+
8
+ def index
9
+ <%= method_name(:load_collection) %>
10
+ end
11
+ <% end -%>
12
+ <% if wui.find_action :show -%>
13
+
14
+ def show
15
+ <%= method_name(:load_object) %>
16
+ end
17
+ <% end -%>
18
+ <% if wui.find_action :new -%>
19
+
20
+ def new
21
+ <%= method_name(:build) %>
22
+ end
23
+ <% end -%>
24
+ <% if wui.find_action :create -%>
25
+
26
+ def create
27
+ <%= method_name(:build) %>
28
+ <%= method_name(:save) %> or render :new
29
+ end
30
+ <% end -%>
31
+ <% if wui.find_action :edit -%>
32
+
33
+ def edit
34
+ <%= method_name(:load_object) %>
35
+ <%= method_name(:build) %>
36
+ end
37
+ <% end -%>
38
+ <% if wui.find_action :update -%>
39
+
40
+ def update
41
+ <%= method_name(:load_object) %>
42
+ <%= method_name(:build) %>
43
+ <%= method_name(:save) %> or render :edit
44
+ end
45
+ <% end -%>
46
+ <% if wui.find_action :destroy -%>
47
+
48
+ def destroy
49
+ <%= method_name(:load_object) %>
50
+ <%= model_name(:ivar) %>.destroy
51
+ redirect_to <%= wui.path(:index) %>
52
+ end
53
+ <% end -%>
54
+ <% wui.custom_actions.each do |action| -%>
55
+
56
+ def <%= action.name %>
57
+ <% if action.member? -%>
58
+ <%= method_name(:load_object) %>
59
+ <%- unless action.get? -%>
60
+ redirect_to <%= model_name(:ivar) %>
61
+ <%- end -%>
62
+ <% elsif action.collection? -%>
63
+ <%= method_name(:load_collection) %>
64
+ <%- unless action.get? -%>
65
+ redirect_to <%= wui.path(:index) %>
66
+ <%- end -%>
67
+ <% end -%>
68
+ end
69
+ <% end -%>
70
+
71
+ private
72
+
73
+ def <%= method_name(:load_collection) %>
74
+ <%= model_name(:ivars) %> ||= <%= method_name(:scope) %>.to_a
75
+ end
76
+
77
+ def <%= method_name(:load_object) %>
78
+ <%= model_name(:ivar) %> ||= <%= method_name(:scope) %>.find(params[:id])
79
+ end
80
+
81
+ def <%= method_name(:build) %>
82
+ <%= model_name(:ivar) %> ||= <%= method_name(:scope) %>.build
83
+ <%= model_name(:ivar) %>.attributes = <%= method_name(:params) %>
84
+ end
85
+
86
+ def <%= method_name(:save) %>
87
+ if <%= model_name(:ivar) %>.save
88
+ redirect_to <%= model_name(:ivar) %>
89
+ end
90
+ end
91
+
92
+ def <%= method_name(:params) %>
93
+ <%= method_name(:params) %> = params[:<%= model_name(:variable) %>]
94
+ <%= method_name(:params) %> ? <%= method_name(:params) %>.permit(<%= wui.model.attrs.map(&:name).map(&:to_sym) %>) : {}
95
+ end
96
+
97
+ def <%= method_name(:scope) %>
98
+ <%= model_name(:class_name) %>.scoped
99
+ end
100
+
101
+ <%- if navigation -%>
102
+ def set_section
103
+ in_sections <%= navigation.section_name(wui) %>
104
+ end
105
+ <%- end -%>
106
+ end
@@ -0,0 +1,57 @@
1
+ # Generate a controller, routes and view files for a WUI.
2
+
3
+ require 'katapult/generator'
4
+ require 'generators/katapult/haml/haml_generator'
5
+
6
+ module Katapult
7
+ module Generators
8
+ class WUIGenerator < Katapult::Generator
9
+
10
+ desc 'Generate a Web User Interface'
11
+
12
+ check_class_collision suffix: 'Controller'
13
+ source_root File.expand_path('../templates', __FILE__)
14
+
15
+
16
+ def create_controller_file
17
+ template 'controller.rb', File.join('app', 'controllers', "#{ model_name(:variables) }_controller.rb")
18
+ end
19
+
20
+ def add_route
21
+ route render_partial('_route.rb')
22
+ end
23
+
24
+ def generate_views
25
+ Generators::HamlGenerator.new(wui).invoke_all
26
+ end
27
+
28
+ no_tasks do
29
+ def method_name(name)
30
+ case name
31
+ when :load_collection then "load_#{model_name :variables}"
32
+ when :load_object then "load_#{model_name :variable}"
33
+ when :build then "build_#{model_name :variable}"
34
+ when :save then "save_#{model_name :variable}"
35
+ when :params then "#{model_name :variable}_params"
36
+ when :scope then "#{model_name :variable}_scope"
37
+ end
38
+ end
39
+
40
+ def model_name(kind = nil)
41
+ wui.model_name(kind)
42
+ end
43
+
44
+ def navigation
45
+ wui.application_model.navigation
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def wui
52
+ @element
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/hash/slice'
3
+
4
+ module Katapult
5
+ end
@@ -0,0 +1,44 @@
1
+ # Models a controller action. To be used within the block of a WUI.
2
+
3
+ require 'katapult/element'
4
+ require 'active_support/core_ext/module/delegation'
5
+ require 'active_support/core_ext/string/inquiry'
6
+
7
+ module Katapult
8
+ class Action < Element
9
+
10
+ options :method, :scope
11
+
12
+ def initialize(*args)
13
+ super
14
+
15
+ self.scope ||= (name == 'index') ? :collection : :member
16
+ set_method
17
+ end
18
+
19
+ delegate :post?, :get?, :put?, to: :method_inquiry
20
+ delegate :member?, :collection?, to: :scope_inquiry
21
+
22
+ private
23
+
24
+ def method_inquiry
25
+ @method.to_s.inquiry
26
+ end
27
+
28
+ def scope_inquiry
29
+ @scope.to_s.inquiry
30
+ end
31
+
32
+ def set_method
33
+ self.method ||= case name
34
+ when 'create', 'update'
35
+ :post
36
+ when 'destroy'
37
+ :delete
38
+ else # index, show, new, edit + custom actions
39
+ :get
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # Internal representation of an application model file.
2
+
3
+ module Katapult
4
+ class ApplicationModel
5
+
6
+ attr_reader :models, :wuis, :navigation
7
+
8
+ def initialize
9
+ @models = []
10
+ @wuis = []
11
+ end
12
+
13
+ def add_model(model)
14
+ model.set_application_model(self)
15
+ @models << model
16
+ end
17
+
18
+ def get_model(name)
19
+ models.find { |m| m.name == name }
20
+ end
21
+
22
+ def add_wui(wui)
23
+ wui.set_application_model(self)
24
+ @wuis << wui
25
+ end
26
+
27
+ def get_wui(name)
28
+ wuis.find { |w| w.name == name }
29
+ end
30
+
31
+ def set_navigation(navigation)
32
+ navigation.set_application_model(self)
33
+ @navigation = navigation
34
+ end
35
+
36
+ # ---
37
+
38
+ def render
39
+ models.each &:render
40
+ wuis.each &:render
41
+ navigation.render if navigation
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,83 @@
1
+ # Models a model attribute. To be used within the block of a model element.
2
+
3
+ require 'katapult/element'
4
+ require 'active_support/core_ext/module/delegation'
5
+ require 'active_support/core_ext/string/inquiry'
6
+ require 'zlib'
7
+
8
+ module Katapult
9
+ class Attribute < Element
10
+
11
+ options :type, :default, :assignable_values, :allow_blank
12
+
13
+ UnknownTypeError = Class.new(StandardError)
14
+ MissingOptionError = Class.new(StandardError)
15
+ TYPES = %i(string email url integer money text markdown flag datetime)
16
+
17
+ def initialize(*args)
18
+ super
19
+
20
+ self.type ||= :email if name.to_s =~ /email/
21
+ self.type ||= :string
22
+
23
+ validate
24
+ end
25
+
26
+ delegate :flag?, to: :type_inquiry
27
+
28
+ def has_defaults?
29
+ default and not [flag?, assignable_values].any?
30
+ end
31
+
32
+ def for_migration
33
+ db_type = case type
34
+ when :email, :url
35
+ 'string'
36
+ when :flag
37
+ 'boolean'
38
+ when :money
39
+ 'decimal{10,2}' # {precision,scale} = total digits, decimal places
40
+ else
41
+ type
42
+ end
43
+
44
+ "#{name}:#{db_type}"
45
+ end
46
+
47
+ def test_value
48
+ if assignable_values
49
+ assignable_values.first
50
+
51
+ else
52
+ case type
53
+ when :string then "#{name}-string"
54
+ when :email then "#{name}@example.com"
55
+ when :url then "#{name}.example.com"
56
+ when :text then "#{name}-text"
57
+
58
+ # Deterministically generate a value from the attribute's name
59
+ when :integer then Zlib.crc32(name).modulo(1000)
60
+ when :money then Zlib.crc32(name).modulo(1000) / 100.0
61
+ when :datetime then Time.at(Zlib.crc32(name))
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def type_inquiry
69
+ @type.to_s.inquiry
70
+ end
71
+
72
+ def validate
73
+ TYPES.include?(type) or raise UnknownTypeError,
74
+ "Attribute type :#{type} is not supported. Use one of #{TYPES.inspect}."
75
+
76
+ if flag? and default.nil?
77
+ raise MissingOptionError,
78
+ "The :flag attribute '#{name}' requires a default (true or false)."
79
+ end
80
+ end
81
+
82
+ end
83
+ end