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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/Rakefile +14 -0
- data/bin/katapult +44 -0
- data/features/binary.feature +48 -0
- data/features/configuration.feature +24 -0
- data/features/katapult.feature +201 -0
- data/features/model.feature +203 -0
- data/features/navigation.feature +80 -0
- data/features/step_definitions/db_steps.rb +8 -0
- data/features/step_definitions/file_steps.rb +14 -0
- data/features/step_definitions/katapult_steps.rb +14 -0
- data/features/step_definitions/rails_steps.rb +44 -0
- data/features/step_definitions/test_steps.rb +7 -0
- data/features/support/env.rb +16 -0
- data/features/wui.feature +319 -0
- data/katapult.gemspec +35 -0
- data/katapult.png +0 -0
- data/lib/generators/katapult/basics/basics_generator.rb +95 -0
- data/lib/generators/katapult/basics/templates/Gemfile +76 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application.css.sass +6 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_all.css.sass +4 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_items.css.sass +11 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_layout.css.sass +26 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_navigation.css.sass +11 -0
- data/lib/generators/katapult/basics/templates/app/assets/stylesheets/application/blocks/_tools.css.sass +12 -0
- data/lib/generators/katapult/basics/templates/config/database.sample.yml +16 -0
- data/lib/generators/katapult/basics/templates/config/database.yml +13 -0
- data/lib/generators/katapult/basics/templates/config/spring.rb +3 -0
- data/lib/generators/katapult/basics/templates/features/support/env-custom.rb +3 -0
- data/lib/generators/katapult/basics/templates/features/support/paths.rb +47 -0
- data/lib/generators/katapult/cucumber_features/cucumber_features_generator.rb +23 -0
- data/lib/generators/katapult/cucumber_features/templates/feature.feature +59 -0
- data/lib/generators/katapult/haml/haml_generator.rb +90 -0
- data/lib/generators/katapult/haml/templates/_form.html.haml +38 -0
- data/lib/generators/katapult/haml/templates/app/views/layouts/application.html.haml +25 -0
- data/lib/generators/katapult/haml/templates/custom_action.html.haml +5 -0
- data/lib/generators/katapult/haml/templates/edit.html.haml +4 -0
- data/lib/generators/katapult/haml/templates/index.html.haml +29 -0
- data/lib/generators/katapult/haml/templates/new.html.haml +4 -0
- data/lib/generators/katapult/haml/templates/show.html.haml +41 -0
- data/lib/generators/katapult/install/install_generator.rb +14 -0
- data/lib/generators/katapult/install/templates/lib/katapult/application_model.rb +18 -0
- data/lib/generators/katapult/model/model_generator.rb +59 -0
- data/lib/generators/katapult/model/templates/app/models/shared/does_flag.rb +32 -0
- data/lib/generators/katapult/model/templates/model.rb +21 -0
- data/lib/generators/katapult/model_specs/model_specs_generator.rb +51 -0
- data/lib/generators/katapult/model_specs/templates/model_spec.rb +34 -0
- data/lib/generators/katapult/navigation/navigation_generator.rb +25 -0
- data/lib/generators/katapult/navigation/templates/app/models/navigation.rb +12 -0
- data/lib/generators/katapult/transform/transform_generator.rb +47 -0
- data/lib/generators/katapult/w_u_i/templates/_route.rb +13 -0
- data/lib/generators/katapult/w_u_i/templates/controller.rb +106 -0
- data/lib/generators/katapult/w_u_i/w_u_i_generator.rb +57 -0
- data/lib/katapult.rb +5 -0
- data/lib/katapult/action.rb +44 -0
- data/lib/katapult/application_model.rb +45 -0
- data/lib/katapult/attribute.rb +83 -0
- data/lib/katapult/element.rb +72 -0
- data/lib/katapult/generator.rb +28 -0
- data/lib/katapult/model.rb +33 -0
- data/lib/katapult/navigation.rb +22 -0
- data/lib/katapult/parser.rb +39 -0
- data/lib/katapult/util.rb +16 -0
- data/lib/katapult/version.rb +3 -0
- data/lib/katapult/wui.rb +77 -0
- data/script/console +16 -0
- data/spec/action_spec.rb +44 -0
- data/spec/attribute_spec.rb +48 -0
- data/spec/model_spec.rb +18 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/util_spec.rb +23 -0
- data/spec/wui_spec.rb +49 -0
- 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
|
data/lib/katapult/wui.rb
ADDED
@@ -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
|
data/script/console
ADDED
@@ -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
|
data/spec/action_spec.rb
ADDED
@@ -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
|
data/spec/model_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|