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