diecut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/diecut +10 -0
- data/gem_test_suite.rb +17 -0
- data/lib/diecut/caller-locations-polyfill.rb +18 -0
- data/lib/diecut/cli.rb +77 -0
- data/lib/diecut/configurable.rb +142 -0
- data/lib/diecut/context-handler.rb +77 -0
- data/lib/diecut/errors.rb +14 -0
- data/lib/diecut/linter.rb +177 -0
- data/lib/diecut/mediator.rb +79 -0
- data/lib/diecut/mill.rb +68 -0
- data/lib/diecut/mustache.rb +20 -0
- data/lib/diecut/plugin-description/context-default.rb +13 -0
- data/lib/diecut/plugin-description/option.rb +63 -0
- data/lib/diecut/plugin-description.rb +147 -0
- data/lib/diecut/plugin-loader.rb +189 -0
- data/lib/diecut/report.rb +136 -0
- data/lib/diecut/template-reducer.rb +88 -0
- data/lib/diecut/template-set.rb +108 -0
- data/lib/diecut/template.rb +58 -0
- data/lib/diecut/ui-applier.rb +71 -0
- data/lib/diecut/ui-config.rb +31 -0
- data/lib/diecut.rb +54 -0
- data/spec/cli_spec.rb +1 -0
- data/spec/configurable_spec.rb +20 -0
- data/spec/linter_spec.rb +132 -0
- data/spec/mill_spec.rb +52 -0
- data/spec/plugin_loader_spec.rb +109 -0
- data/spec/register_plugin_spec.rb +105 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/template-reducer_spec.rb +36 -0
- data/spec/template_set_spec.rb +22 -0
- data/spec/template_spec.rb +60 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0db4143a62b998da4c0a37dca8bd1c10dfdc083e
|
4
|
+
data.tar.gz: 3df81689d7dd8c921dfba82edaed9aeaa5a60773
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8c8ad0152418a96ffde7d7a262d24868ff18c1604bed78699c618726d80806792eb5c344f1946af855ec9f7bf0599be0100878c7d3deecd2ffdf7718d86466d2
|
7
|
+
data.tar.gz: 524f260e59f3ee1375f432f9880d79e4aa9976ed127fb35a0767b7a40768943fc97ae741ceecf9c66c5567b946f9385dd85335fdac060e2aba57ff7af6e6cbbb
|
data/bin/diecut
ADDED
data/gem_test_suite.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
puts Dir::pwd
|
2
|
+
require 'test/unit'
|
3
|
+
begin
|
4
|
+
require 'spec'
|
5
|
+
rescue LoadError
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
class RSpecTest < Test::Unit::TestCase
|
10
|
+
def test_that_rspec_is_available
|
11
|
+
assert_nothing_raised("\n\n * RSpec isn't available - please run: gem install rspec *\n\n"){ ::Spec }
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_that_specs_pass
|
15
|
+
assert(system(*%w{spec -f e -p **/*.rb spec}),"\n\n * Specs failed *\n\n")
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Diecut
|
2
|
+
module CallerLocationsPolyfill
|
3
|
+
unless Kernel.instance_method(:caller_locations)
|
4
|
+
# :nocov:
|
5
|
+
FakeLocation = Struct.new(:absolute_path, :lineno, :label)
|
6
|
+
LINE_RE = %r[(?<absolute_path>[^:]):(?<lineno>\d+):(?:in `(?<label>[^'])')?]
|
7
|
+
# covers exactly the use cases we need
|
8
|
+
def caller_locations(range, length=nil)
|
9
|
+
caller[range.begin+1..range.end+1].map do |line|
|
10
|
+
if m = LINE_RE.match(line)
|
11
|
+
FakeLocation.new(m.named_captures.values_at("absolute_path", "lineno", "label"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
# :nocov:
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/diecut/cli.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'diecut'
|
3
|
+
require 'diecut/mill'
|
4
|
+
|
5
|
+
module Diecut
|
6
|
+
class KindCli < Thor
|
7
|
+
include Thor::Actions
|
8
|
+
|
9
|
+
desc "generate TARGET", "Generate code"
|
10
|
+
def generate(target_dir)
|
11
|
+
self.destination_root = target_dir
|
12
|
+
|
13
|
+
mill = Mill.new(self.class.kind)
|
14
|
+
mill.activate_plugins {|name| options["with-#{name}"] }
|
15
|
+
|
16
|
+
ui = mill.user_interface
|
17
|
+
options.delete_if{|_, value| value.nil?}
|
18
|
+
ui.from_hash(options)
|
19
|
+
|
20
|
+
mill.churn(ui) do |path, contents|
|
21
|
+
create_file(path, contents)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
method_option :all_on => false
|
26
|
+
desc "lint", "Check well-formed-ness of code generators"
|
27
|
+
def lint
|
28
|
+
require 'diecut/linter'
|
29
|
+
mill = Mill.new(self.class.kind)
|
30
|
+
if options["all_on"]
|
31
|
+
mill.activate_plugins{ true }
|
32
|
+
else
|
33
|
+
mill.activate_plugins {|name| options["with-#{name}"] }
|
34
|
+
end
|
35
|
+
|
36
|
+
puts Linter.new(mill).report
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class CommandLine < Thor
|
41
|
+
def self.build_kind_subcommand(plugin_kind)
|
42
|
+
mediator = Diecut.mediator(plugin_kind)
|
43
|
+
example_ui = mediator.build_example_ui
|
44
|
+
|
45
|
+
klass = Class.new(KindCli) do
|
46
|
+
class << self
|
47
|
+
def kind(value = nil)
|
48
|
+
if @kind.nil?
|
49
|
+
@kind = value
|
50
|
+
end
|
51
|
+
@kind
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
mediator.plugins.each do |plugin|
|
56
|
+
class_option "with-#{plugin.name}", :default => plugin.default_active?
|
57
|
+
end
|
58
|
+
|
59
|
+
example_ui.field_names.each do |field|
|
60
|
+
method_option(field, {:for => :generate, :desc => example_ui.description(field) || field,
|
61
|
+
:required => example_ui.required?(field), :default => example_ui.default_for(field)})
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
klass.kind(plugin_kind)
|
66
|
+
|
67
|
+
klass
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.add_kind(kind)
|
71
|
+
desc "#{kind}", "Commands related to templating for #{kind}"
|
72
|
+
kind_class = build_kind_subcommand(kind)
|
73
|
+
const_set(kind.sub(/\A./){|match| match.upcase }, kind_class)
|
74
|
+
subcommand kind, kind_class
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'calibrate'
|
2
|
+
module Diecut
|
3
|
+
class Configurable
|
4
|
+
include Calibrate::Configurable
|
5
|
+
module ClassMethods
|
6
|
+
attr_accessor :target_name
|
7
|
+
|
8
|
+
def build_subclass(name)
|
9
|
+
Class.new(self).tap{|cc| cc.target_name = name }
|
10
|
+
end
|
11
|
+
|
12
|
+
def classname
|
13
|
+
name || superclass.name
|
14
|
+
end
|
15
|
+
|
16
|
+
def deep_field_names
|
17
|
+
field_names.map do |name|
|
18
|
+
field_value = field_metadata(name).default_value
|
19
|
+
if field_value==self
|
20
|
+
return ["LOOPED"]
|
21
|
+
end
|
22
|
+
if field_value.is_a?(Class) and field_value < Diecut::Configurable
|
23
|
+
field_value.deep_field_names.map do |subname|
|
24
|
+
"#{name}.#{subname}"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
name
|
28
|
+
end
|
29
|
+
end.flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
return "#<#{classname}:#{target_name}:(#{deep_field_names.join(",")})>"
|
34
|
+
end
|
35
|
+
|
36
|
+
def absorb_context(from)
|
37
|
+
from.field_names.each do |name|
|
38
|
+
from_metadata = from.field_metadata(name)
|
39
|
+
from_value = from_metadata.default_value
|
40
|
+
into_metadata = field_metadata(name)
|
41
|
+
|
42
|
+
if into_metadata.nil?
|
43
|
+
if from_value.is_a?(Class) and from_value < Calibrate::Configurable
|
44
|
+
nested = build_subclass("#{target_name}.#{name}")
|
45
|
+
setting(name, nested)
|
46
|
+
nested.absorb_context(from_value)
|
47
|
+
else
|
48
|
+
if from_metadata.is?(:required)
|
49
|
+
setting(name)
|
50
|
+
else
|
51
|
+
setting(name, from_value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
next
|
55
|
+
end
|
56
|
+
into_value = into_metadata.default_value
|
57
|
+
if into_value.is_a?(Class) and into_value < Calibrate::Configurable
|
58
|
+
if from_value.is_a?(Class) and from_value < Calibrate::Configurable
|
59
|
+
into_value.absorb_context(from_value)
|
60
|
+
else
|
61
|
+
raise "Field clash: #{name.inspect} is already a complex value, but a simple value in the absorbed configurable"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
unless from_value.is_a?(Class) and from_value < Calibrate::Configurable
|
65
|
+
# Noop - maybe should compare the default values? - should always
|
66
|
+
# be nil right now...
|
67
|
+
else
|
68
|
+
raise "Field clash: #{name.inspect} is already a simple value, but a complex value on the absorbed configurable"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def walk_path(field_path)
|
75
|
+
first, *rest = *field_path
|
76
|
+
|
77
|
+
segment = PathSegment.new(self, first.to_sym)
|
78
|
+
if rest.empty?
|
79
|
+
[segment]
|
80
|
+
else
|
81
|
+
[segment] + segment.nested.walk_path(rest)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_setting(field, is_section = false)
|
86
|
+
nested = walk_path(field).last.klass
|
87
|
+
|
88
|
+
if is_section
|
89
|
+
nested.setting(field.last, build_subclass("#{target_name}.#{field.last}"))
|
90
|
+
else
|
91
|
+
nested.setting(field.last)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
extend ClassMethods
|
96
|
+
|
97
|
+
def walk_path(field_path)
|
98
|
+
first, *rest = *field_path
|
99
|
+
|
100
|
+
segment = InstanceSegment.new(self, first.to_sym)
|
101
|
+
if rest.empty?
|
102
|
+
[segment]
|
103
|
+
else
|
104
|
+
[segment] + segment.value.walk_path(rest)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
class InstanceSegment < ::Struct.new(:instance, :name)
|
110
|
+
def metadata
|
111
|
+
@metadata ||= instance.class.field_metadata(name)
|
112
|
+
end
|
113
|
+
|
114
|
+
def value
|
115
|
+
metadata.value_on(instance)
|
116
|
+
end
|
117
|
+
|
118
|
+
def value=(value)
|
119
|
+
instance.__send__(metadata.writer_method, value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class PathSegment < ::Struct.new(:klass, :name)
|
124
|
+
def metadata
|
125
|
+
@metadata ||= klass.field_metadata(name)
|
126
|
+
end
|
127
|
+
|
128
|
+
def nested
|
129
|
+
@nested ||=
|
130
|
+
begin
|
131
|
+
if metadata.nil?
|
132
|
+
nested = Configurable.build_subclass("#{klass.target_name}.#{name}")
|
133
|
+
klass.setting(name, nested)
|
134
|
+
nested
|
135
|
+
else
|
136
|
+
metadata.default_value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'diecut/errors'
|
2
|
+
module Diecut
|
3
|
+
class ContextHandler
|
4
|
+
attr_accessor :context_class, :ui_class, :plugins
|
5
|
+
|
6
|
+
def apply_simple_defaults
|
7
|
+
plugins.each do |plugin|
|
8
|
+
plugin.context_defaults.each do |default|
|
9
|
+
next unless default.simple?
|
10
|
+
begin
|
11
|
+
apply_simple_default(default)
|
12
|
+
rescue Error
|
13
|
+
raise Error, "Plugin #{plugin.name.inspect} failed"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_to_ui
|
20
|
+
plugins.each do |plugin|
|
21
|
+
plugin.options.each do |option|
|
22
|
+
apply_option_to_ui(option)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def backfill_options_to_context
|
28
|
+
plugins.each do |plugin|
|
29
|
+
plugin.options.each do |option|
|
30
|
+
backfill_to_context(option)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def backfill_to_context(option)
|
36
|
+
return unless option.has_context_path?
|
37
|
+
|
38
|
+
segment = context_class.walk_path(option.context_path).last
|
39
|
+
if option.has_default?
|
40
|
+
segment.klass.setting(segment.name, option.default_value)
|
41
|
+
else
|
42
|
+
segment.klass.setting(segment.name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply_simple_default(default)
|
47
|
+
target = context_class.walk_path(default.context_path).last
|
48
|
+
if target.metadata.nil?
|
49
|
+
raise UnusedDefault, "No template uses a value at #{default.context_path.inspect}"
|
50
|
+
else
|
51
|
+
target.metadata.default_value = default.value
|
52
|
+
target.metadata.is(:defaulting)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def apply_option_to_ui(option)
|
57
|
+
ui_class.options_hash[option.name] = option
|
58
|
+
|
59
|
+
if option.has_context_path?
|
60
|
+
context_metadata = context_class.walk_path(option.context_path).last.metadata
|
61
|
+
if option.has_default?
|
62
|
+
ui_class.setting(option.name, option.default_value)
|
63
|
+
elsif context_metadata.is?(:defaulting)
|
64
|
+
ui_class.setting(option.name, context_metadata.default_value)
|
65
|
+
else
|
66
|
+
ui_class.setting(option.name)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
if option.has_default?
|
70
|
+
ui_class.setting(option.name, option.default_value)
|
71
|
+
else
|
72
|
+
ui_class.setting(option.name)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Diecut
|
2
|
+
class Error < RuntimeError;
|
3
|
+
def message
|
4
|
+
if cause.nil?
|
5
|
+
super
|
6
|
+
else
|
7
|
+
super + " because: #{cause.message}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
class UnusedDefault < Error; end
|
12
|
+
class OverriddenDefault < Error; end
|
13
|
+
class InvalidConfig < Error; end
|
14
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'diecut/report'
|
2
|
+
|
3
|
+
module Diecut
|
4
|
+
class Linter
|
5
|
+
def initialize(mill)
|
6
|
+
@mill = mill
|
7
|
+
end
|
8
|
+
attr_reader :mill
|
9
|
+
|
10
|
+
def report
|
11
|
+
@ui = mill.user_interface
|
12
|
+
|
13
|
+
formatter = ReportFormatter.new([
|
14
|
+
option_collision_report,
|
15
|
+
orphaned_fields,
|
16
|
+
overridden_context_defaults
|
17
|
+
])
|
18
|
+
|
19
|
+
formatter.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
# Needed:
|
23
|
+
# Overridden option defaults (without plugin dep)
|
24
|
+
# Option with default, context with default (w/o PD)
|
25
|
+
|
26
|
+
def unindent(text)
|
27
|
+
indent = text.scan(/(^[ \t]*)\S/).map{|cap| cap.first}.max_by(&:length)
|
28
|
+
text.gsub(%r{^#{indent}},'')
|
29
|
+
end
|
30
|
+
|
31
|
+
def each_plugin
|
32
|
+
mill.mediator.activated_plugins.each do |plugin|
|
33
|
+
yield plugin
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def each_default
|
38
|
+
each_plugin do |plugin|
|
39
|
+
plugin.context_defaults.each do |default|
|
40
|
+
yield default, plugin
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def each_option
|
46
|
+
each_plugin do |plugin|
|
47
|
+
plugin.options.each do |option|
|
48
|
+
yield option, plugin
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def overridden_context_defaults
|
54
|
+
Report.new("Overridden context defaults", ["Output field", "Default value", "Source plugin"]).tap do |report|
|
55
|
+
default_values = Hash.new{|h,k| h[k]=[]}
|
56
|
+
each_default do |default, plugin|
|
57
|
+
next unless default.simple?
|
58
|
+
|
59
|
+
default_values[default.context_path] << [default, plugin]
|
60
|
+
end
|
61
|
+
|
62
|
+
default_values.each do |key, set|
|
63
|
+
default_values[key] = set.find_all do |plugin|
|
64
|
+
!set.any?{|child|
|
65
|
+
next if child == plugin
|
66
|
+
Diecut.plugin_loader.strict_sequence?(plugin[1], child[1])
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
default_values.each_value do |set|
|
72
|
+
if set.length > 1
|
73
|
+
set.each do |default, plugin|
|
74
|
+
|
75
|
+
report.add(default.context_path.join("."), default.value, plugin.name)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
unless report.empty?
|
81
|
+
report.fail("Multiple plugins assign different values to be rendered")
|
82
|
+
report.advice = unindent(<<-EOA)
|
83
|
+
This is a problem because each plugin may be assuming it's default
|
84
|
+
value, and since there's no guarantee in which order the plugins are
|
85
|
+
loaded, the actual default value is difficult to predict. In general,
|
86
|
+
this kind of override behavior can be difficult to reason about.
|
87
|
+
|
88
|
+
Either the collision is accidental, in which case the default value
|
89
|
+
should be removed from one plugin or the other. If the override is
|
90
|
+
intentional, then the overriding plugin's gem should depend on the
|
91
|
+
overridden one's - since you are overriding the value intentionally,
|
92
|
+
it makes sense to ensure that the value is there to override. Diecut
|
93
|
+
will load plugins such that the dependant plugins are loaded later,
|
94
|
+
which solves the predictability problem.
|
95
|
+
EOA
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def option_collision_report
|
101
|
+
Report.new("Option collisions", ["Output target", "Option name", "Source plugin"]).tap do |report|
|
102
|
+
option_targets = Hash.new{|h,k| h[k]=[]}
|
103
|
+
each_option do |option, plugin|
|
104
|
+
next unless option.has_context_path?
|
105
|
+
option_targets[option.context_path] << [plugin, option]
|
106
|
+
end
|
107
|
+
option_targets.each_value do |set|
|
108
|
+
if set.length > 1
|
109
|
+
set.each do |plugin, option|
|
110
|
+
report.add(option.context_path.join("."), option.name, plugin.name)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
unless report.empty?
|
116
|
+
report.fail("Multiple options assign the same values to be rendered")
|
117
|
+
report.advice = unindent(<<-EOA)
|
118
|
+
This is problem because two options in the user interface both change
|
119
|
+
rendered values. If a user supplies both with different values, the
|
120
|
+
output isn't predictable (either one might take effect).
|
121
|
+
|
122
|
+
Most likely, this is a simple error: remove options from each group
|
123
|
+
that targets the same rendered value until only one remains. It may
|
124
|
+
also be that one option has a typo - that there's a rendering target
|
125
|
+
that's omitted.
|
126
|
+
EOA
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def orphaned_fields
|
132
|
+
Report.new("Template fields all have settings", ["Output field", "Source file"]).tap do |report|
|
133
|
+
context_class = mill.context_class
|
134
|
+
|
135
|
+
required_fields = {}
|
136
|
+
|
137
|
+
context_class.field_names.each do |field_name|
|
138
|
+
if context_class.field_metadata(field_name).is?(:required)
|
139
|
+
required_fields[field_name.to_s] = []
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
mill.templates.all_templates.each do |template|
|
144
|
+
template.reduced.leaf_fields.each do |field|
|
145
|
+
field = field.join(".")
|
146
|
+
if required_fields.has_key?(field)
|
147
|
+
required_fields[field] << template.path
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
each_option do |option, plugin|
|
153
|
+
next unless option.has_context_path?
|
154
|
+
field = option.context_path.join(".")
|
155
|
+
required_fields.delete(field)
|
156
|
+
end
|
157
|
+
|
158
|
+
required_fields.each do |name, targets|
|
159
|
+
targets.each do |target|
|
160
|
+
report.add(name, target)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
unless report.empty?
|
165
|
+
report.status = "WARN"
|
166
|
+
report.advice = unindent(<<-EOA)
|
167
|
+
These fields might not receive a value during generation, which will
|
168
|
+
raise an error at use time.
|
169
|
+
|
170
|
+
It's possible these fields are set in a resolve block in one of the
|
171
|
+
plugins - Diecut can't check for that yet.
|
172
|
+
EOA
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'diecut/errors'
|
2
|
+
require 'diecut/ui-config'
|
3
|
+
require 'diecut/ui-applier'
|
4
|
+
require 'diecut/context-handler'
|
5
|
+
|
6
|
+
module Diecut
|
7
|
+
class Mediator
|
8
|
+
def initialize
|
9
|
+
@plugins = []
|
10
|
+
@activated = {}
|
11
|
+
end
|
12
|
+
attr_reader :plugins
|
13
|
+
|
14
|
+
def add_plugin(plug)
|
15
|
+
@activated[plug.name] = plug.default_activated
|
16
|
+
@plugins << plug
|
17
|
+
end
|
18
|
+
|
19
|
+
def activated?(plug_name)
|
20
|
+
@activated[plug_name]
|
21
|
+
end
|
22
|
+
|
23
|
+
def activate(plug_name)
|
24
|
+
@activated[plug_name] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def deactivate(plug_name)
|
28
|
+
@activated[plug_name] = false
|
29
|
+
end
|
30
|
+
|
31
|
+
def activated_plugins
|
32
|
+
@plugins.find_all do |plugin|
|
33
|
+
@activated[plugin.name]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set up context default settings
|
38
|
+
# set up ui settings from context
|
39
|
+
#
|
40
|
+
# < User gets involved >
|
41
|
+
#
|
42
|
+
def build_example_ui
|
43
|
+
ui_class = Class.new(UIConfig)
|
44
|
+
|
45
|
+
handler = ContextHandler.new
|
46
|
+
handler.context_class = Class.new(Configurable)
|
47
|
+
handler.ui_class = ui_class
|
48
|
+
handler.plugins = @plugins
|
49
|
+
|
50
|
+
handler.backfill_options_to_context
|
51
|
+
handler.apply_to_ui
|
52
|
+
|
53
|
+
handler.ui_class
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_ui_class(context_class)
|
57
|
+
ui_class = Class.new(UIConfig)
|
58
|
+
|
59
|
+
handler = ContextHandler.new
|
60
|
+
handler.context_class = context_class
|
61
|
+
handler.ui_class = ui_class
|
62
|
+
handler.plugins = activated_plugins
|
63
|
+
|
64
|
+
handler.apply_simple_defaults
|
65
|
+
handler.apply_to_ui
|
66
|
+
|
67
|
+
handler.ui_class
|
68
|
+
end
|
69
|
+
|
70
|
+
def apply_user_input(ui, context_class)
|
71
|
+
applier = UIApplier.new
|
72
|
+
applier.plugins = activated_plugins
|
73
|
+
applier.ui = ui
|
74
|
+
applier.context = context_class.new
|
75
|
+
applier.apply
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/diecut/mill.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'valise'
|
2
|
+
require 'diecut'
|
3
|
+
require 'diecut/template-set'
|
4
|
+
|
5
|
+
module Diecut
|
6
|
+
class Mill
|
7
|
+
def initialize(kind)
|
8
|
+
@kind = kind
|
9
|
+
end
|
10
|
+
attr_reader :kind
|
11
|
+
attr_writer :valise, :mediator, :templates
|
12
|
+
|
13
|
+
def mediator
|
14
|
+
@mediator ||= Diecut.mediator(kind)
|
15
|
+
end
|
16
|
+
|
17
|
+
def templates
|
18
|
+
@templates ||= TemplateSet.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def activate_plugins
|
22
|
+
mediator.plugins.map(&:name).each do |name|
|
23
|
+
if yield(name)
|
24
|
+
mediator.activate(name)
|
25
|
+
else
|
26
|
+
mediator.deactivate(name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def valise
|
32
|
+
@valise ||= mediator.activated_plugins.map do |plugin|
|
33
|
+
stem = plugin.stem_for(kind)
|
34
|
+
Valise::Set.define do
|
35
|
+
ro stem.template_dir
|
36
|
+
end.stemmed(stem.stem)
|
37
|
+
end.reduce{|left, right| left + right}.sub_set(kind)
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_files
|
41
|
+
valise.filter('**', %i[extended dotmatch]).files do |file|
|
42
|
+
templates.add(file.rel_path.to_s, file.contents)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def context_class
|
47
|
+
templates.context_class
|
48
|
+
end
|
49
|
+
|
50
|
+
def ui_class
|
51
|
+
mediator.build_ui_class(context_class)
|
52
|
+
end
|
53
|
+
|
54
|
+
def user_interface
|
55
|
+
load_files
|
56
|
+
templates.prepare
|
57
|
+
|
58
|
+
ui_class.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def churn(ui)
|
62
|
+
templates.context = mediator.apply_user_input(ui, templates.context_class)
|
63
|
+
templates.results do |path, contents|
|
64
|
+
yield(path, contents)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|