katapult 0.1.0

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