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,72 @@
1
+ # The base class for all katapult elements to inherit from.
2
+
3
+ # Every katapult element has a name which is a String. All options passed will
4
+ # be mapped to attributes. Afterwards, the optional block will be yielded with
5
+ # self.
6
+
7
+ module Katapult
8
+ class Element
9
+
10
+ UnknownOptionError = Class.new(StandardError)
11
+
12
+ attr_accessor :name, :options
13
+ attr_reader :application_model
14
+
15
+ # Improve semantics in element classes
16
+ class << self
17
+ alias_method :options, :attr_accessor
18
+ end
19
+
20
+
21
+ def initialize(name, options = {})
22
+ self.name = name.to_s
23
+ self.options = options
24
+
25
+ set_attributes(options)
26
+
27
+ yield(self) if block_given?
28
+ end
29
+
30
+ def set_application_model(app_model)
31
+ @application_model = app_model
32
+ end
33
+
34
+ def name(kind = nil)
35
+ human_name = @name.downcase
36
+ machine_name = @name.underscore
37
+
38
+ case kind.to_s
39
+ when 'symbol' then ":#{machine_name}"
40
+ when 'symbols' then ":#{machine_name.pluralize}"
41
+ when 'variable' then machine_name
42
+ when 'variables' then machine_name.pluralize
43
+ when 'ivar' then "@#{machine_name}"
44
+ when 'ivars' then "@#{machine_name.pluralize}"
45
+ when 'human_plural' then human_name.pluralize
46
+ when 'human' then human_name
47
+ when 'class' then machine_name.classify
48
+ when 'classes' then machine_name.classify.pluralize
49
+ else
50
+ @name
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Map options to attributes.
57
+ # Example: set_attributes(foo: 123) sets the :foo attribute to 123 (via
58
+ # #foo=) and raises UnknownOptionError if the attribute does not exist.
59
+ def set_attributes(options)
60
+ options.each_pair do |option, value|
61
+ setter = "#{option}="
62
+
63
+ if respond_to? setter
64
+ send(setter, value)
65
+ else
66
+ raise UnknownOptionError, "#{self.class.name} does not support option #{option.inspect}."
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,28 @@
1
+ # The katapult generator base class, slightly adapted from Rails generators.
2
+
3
+ require 'rails/generators'
4
+
5
+ module Katapult
6
+ class Generator < Rails::Generators::NamedBase
7
+
8
+ attr_accessor :element
9
+
10
+ def initialize(element)
11
+ self.element = element
12
+
13
+ super([element.name], {}, {}) # args, opts, config
14
+ end
15
+
16
+ private
17
+
18
+ def app_name
19
+ File.basename(Dir.pwd)
20
+ end
21
+
22
+ def render_partial(template_path, given_binding = nil)
23
+ path = File.join(self.class.source_root, template_path)
24
+ ERB.new(::File.binread(path), nil, '%').result(given_binding || binding)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # Models a Rails model.
2
+
3
+ require 'katapult/element'
4
+ require 'katapult/attribute'
5
+ require 'generators/katapult/model/model_generator'
6
+
7
+ module Katapult
8
+ class Model < Element
9
+
10
+ UnknownAttributeError = Class.new(StandardError)
11
+
12
+ attr_accessor :attrs
13
+
14
+ def initialize(*args)
15
+ self.attrs = []
16
+
17
+ super
18
+ end
19
+
20
+ def attr(attr_name, options = {})
21
+ attrs << Attribute.new(attr_name, options)
22
+ end
23
+
24
+ def label_attr
25
+ attrs.first
26
+ end
27
+
28
+ def render
29
+ Generators::ModelGenerator.new(self).invoke_all
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # Models a navigation.
2
+
3
+ require 'katapult/element'
4
+ require 'generators/katapult/navigation/navigation_generator'
5
+
6
+ module Katapult
7
+ class Navigation < Element
8
+
9
+ def wuis
10
+ application_model.wuis
11
+ end
12
+
13
+ def section_name(wui)
14
+ wui.model_name(:symbols)
15
+ end
16
+
17
+ def render
18
+ Generators::NavigationGenerator.new(self).invoke_all
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # This class reads an application model file and turns it into an
2
+ # ApplicationModel instance.
3
+
4
+ require_relative 'application_model'
5
+ require 'katapult/model'
6
+ require 'katapult/wui'
7
+ require 'katapult/navigation'
8
+
9
+ module Katapult
10
+ class Parser
11
+
12
+ def initialize
13
+ self.application_model = Katapult::ApplicationModel.new
14
+ end
15
+
16
+ def parse(path_to_app_model_file)
17
+ instance_eval File.read(path_to_app_model_file), path_to_app_model_file
18
+
19
+ application_model
20
+ end
21
+
22
+ def model(name, options = {}, &block)
23
+ application_model.add_model Model.new(name, options, &block)
24
+ end
25
+
26
+ def wui(name, options = {}, &block)
27
+ application_model.add_wui WUI.new(name, options, &block)
28
+ end
29
+
30
+ def navigation(name)
31
+ application_model.set_navigation Navigation.new(name)
32
+ end
33
+
34
+ private
35
+
36
+ attr_accessor :application_model
37
+
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # Utility methods.
2
+
3
+ # The Katapult::Util module is used inside the `katapult` script. It should not
4
+ # require any gems in order to prevent version conflicts.
5
+
6
+ module Katapult
7
+ module Util
8
+ extend self
9
+
10
+ def git_commit(message)
11
+ message.gsub! /'/, "" # remove single quotes
12
+ system "git add --all; git commit -m '#{ message }' --author='katapult <katapult@makandra.com>'"
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Katapult
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,77 @@
1
+ # Models a controller, including routes and views. Little more than a container
2
+ # class for the Attribute element.
3
+
4
+ require 'katapult/element'
5
+ require 'katapult/action'
6
+ require 'generators/katapult/w_u_i/w_u_i_generator'
7
+
8
+ module Katapult
9
+ class WUI < Element
10
+
11
+ options :model
12
+ attr_accessor :actions
13
+
14
+ RAILS_ACTIONS = %w[ index show new create edit update destroy ]
15
+ UnknownActionError = Class.new(StandardError)
16
+ UnknownModelError = Class.new(StandardError)
17
+
18
+ def initialize(*args)
19
+ self.actions = []
20
+
21
+ super
22
+ end
23
+
24
+ # DSL
25
+ def action(name, options = {})
26
+ actions << Action.new(:new, options) if name.to_s == 'create'
27
+ actions << Action.new(:edit, options) if name.to_s == 'update'
28
+
29
+ actions << Action.new(name, options)
30
+ end
31
+
32
+ # DSL
33
+ def crud
34
+ %i(index show create update destroy).each &method(:action)
35
+ end
36
+
37
+ def model
38
+ model_name = @model || self.name
39
+ application_model.get_model(model_name) or raise UnknownModelError,
40
+ "Could not find a model named #{model_name}"
41
+ end
42
+
43
+ def custom_actions
44
+ actions.reject { |a| RAILS_ACTIONS.include? a.name }
45
+ end
46
+
47
+ def find_action(action_name)
48
+ actions.find { |a| a.name == action_name.to_s }
49
+ end
50
+
51
+ def path(action, object_name = nil)
52
+ unless action.is_a?(Action)
53
+ not_found_message = "Unknown action '#{action}'"
54
+ action = find_action(action) or raise UnknownActionError, not_found_message
55
+ end
56
+
57
+ member_path = "#{model.name(:variable)}_path"
58
+ collection_path = "#{model.name(:variables)}_path"
59
+
60
+ path = ''
61
+ path << action.name << '_' unless %w[index show destroy].include?(action.name)
62
+ path << (action.member? ? member_path : collection_path)
63
+ path << "(#{object_name})" if object_name
64
+
65
+ path
66
+ end
67
+
68
+ def model_name(kind = nil)
69
+ model.name(kind)
70
+ end
71
+
72
+ def render
73
+ Generators::WUIGenerator.new(self).invoke_all
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file imitates the Rails console to ease debugging the gem.
4
+ # Run `script/console` from the gem root to start an IRB with the gem loaded.
5
+
6
+ # Remember to require classes before using them,
7
+ # i.e. run `require 'katapult/wui'` before `Katapult::WUI.new :example_name`.
8
+
9
+ irb_options = [
10
+ '-Ilib', # add lib/ to load_path
11
+ '-d', # set $DEBUG = true
12
+ '-rkatapult', # require katapult
13
+ '-f' # don't read ~/.irbrc
14
+ ]
15
+
16
+ exec 'irb', *irb_options
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'katapult/action'
3
+
4
+ describe Katapult::Action do
5
+
6
+ subject { described_class.new 'action' }
7
+
8
+ describe '#post?' do
9
+ it 'returns true when its method is :post' do
10
+ subject.method = :post
11
+ expect(subject.post?).to be true
12
+ end
13
+
14
+ it 'returns false when its method is :get' do
15
+ subject.method = :get
16
+ expect(subject.post?).to be false
17
+ end
18
+ end
19
+
20
+ describe '#member?' do
21
+ it 'returns true when its scope is :member' do
22
+ subject.scope = :member
23
+ expect(subject.member?).to be true
24
+ end
25
+
26
+ it 'returns false when its scope is :collection' do
27
+ subject.scope = :collection
28
+ expect(subject.member?).to be false
29
+ end
30
+
31
+ it 'returns false when it is an index action' do
32
+ subject = described_class.new :index
33
+ expect(subject.member?).to be false
34
+ end
35
+ end
36
+
37
+ describe '#collection?' do
38
+ it 'returns true when it is an index action' do
39
+ subject = described_class.new :index
40
+ expect(subject.collection?).to be true
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+ require 'katapult/attribute'
3
+
4
+ describe Katapult::Attribute do
5
+
6
+ it 'is of type :string by default' do
7
+ expect(described_class.new('name').type).to eql(:string)
8
+ expect(described_class.new('address').type).to eql(:string)
9
+ end
10
+
11
+ it 'requires a default for :flag attributes' do
12
+ expect do
13
+ described_class.new('attr', type: :flag)
14
+ end.to raise_error(Katapult::Attribute::MissingOptionError,
15
+ "The :flag attribute 'attr' requires a default (true or false).")
16
+ end
17
+
18
+ describe '#flag?' do
19
+ it 'returns whether it is of type :flag' do
20
+ expect(described_class.new('attr', type: :flag, default: false).flag?).to be true
21
+ expect(described_class.new('attr', type: :string).flag?).to be false
22
+ end
23
+ end
24
+
25
+ describe 'available types' do
26
+ it 'raises an error if the specified type is not supported' do
27
+ expect do
28
+ described_class.new('attr', type: :undefined)
29
+ end.to raise_error(Katapult::Attribute::UnknownTypeError,
30
+ "Attribute type :undefined is not supported. Use one of #{Katapult::Attribute::TYPES.inspect}."
31
+ )
32
+ end
33
+ end
34
+
35
+ describe 'email attributes' do
36
+ it 'recognizes email attributes' do
37
+ expect(described_class.new('email').type).to eql(:email)
38
+ expect(described_class.new('customer_email').type).to eql(:email)
39
+ expect(described_class.new('name').type).to_not eql(:email)
40
+ end
41
+
42
+ it 'does not overwrite a given type' do
43
+ expect(described_class.new('email', type: :url).type).to eql(:url)
44
+ expect(described_class.new('email_updated_at', type: :datetime).type).to eql(:datetime)
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'katapult/model'
3
+ require 'katapult/attribute'
4
+
5
+ describe Katapult::Model do
6
+
7
+ subject { described_class.new('model') }
8
+
9
+ describe '#label_attr' do
10
+ it 'returns the model’s first attribute' do
11
+ subject.attr('first_attr')
12
+ subject.attr('second_attr')
13
+
14
+ expect(subject.label_attr.name).to eql('first_attr')
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'pry'
5
+
6
+ # Require some Rails for the specs to pass
7
+ require 'active_support/core_ext/string/inflections'
8
+
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+
14
+ # Run specs in random order to surface order dependencies. If you find an
15
+ # order dependency and want to debug it, you can fix the order by providing
16
+ # the seed, which is printed after each run.
17
+ # --seed 1234
18
+ config.order = 'random'
19
+ end