diecut 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ require 'tsort'
2
+ require 'diecut/template'
3
+ require 'diecut/mustache'
4
+ require 'diecut/configurable'
5
+
6
+ module Diecut
7
+ class TemplateSet
8
+ include TSort
9
+
10
+ def initialize
11
+ @templates = {}
12
+ @path_templates = {}
13
+ @breaking_cycles = {}
14
+ @partials = {}
15
+ @context_class = nil
16
+ @context = nil
17
+ @renderer = nil
18
+ end
19
+ attr_reader :partials, :templates, :path_templates
20
+
21
+ def add(path, contents)
22
+ template = Diecut::Template.new(path, contents)
23
+ @templates[path] = template
24
+ path_template = Diecut::Template.new("path for " + path, path)
25
+ @path_templates[path] = path_template
26
+ template.partials.each do |name, _|
27
+ @partials[name] = template
28
+ end
29
+ end
30
+
31
+ def all_templates
32
+ @templates.values + @path_templates.values
33
+ end
34
+
35
+ def context_class
36
+ @context_class ||= Configurable.build_subclass("General context")
37
+ end
38
+
39
+ def context
40
+ @context ||= context_class.new
41
+ end
42
+ attr_writer :context
43
+
44
+ def is_partial?(tmpl)
45
+ @partials.has_key?(tmpl.path)
46
+ end
47
+
48
+ def tsort_each_node(&block)
49
+ @breaking_cycles.clear
50
+ @templates.each_value(&block)
51
+ end
52
+
53
+ def tsort_each_child(node)
54
+ node.partials.each do |name, _|
55
+ unless @breaking_cycles[name]
56
+ @breaking_cycles[name] = true
57
+ yield @templates[name]
58
+ end
59
+ end
60
+ end
61
+
62
+ def prepare
63
+ associate_partials
64
+ build_context
65
+ end
66
+
67
+ def renderer
68
+ @renderer ||= Mustache.new.tap do |mustache|
69
+ mustache.partials_hash = partials
70
+ end
71
+ end
72
+
73
+ def results
74
+ context.check_required
75
+
76
+ tsort_each do |template|
77
+ next if is_partial?(template)
78
+
79
+ path = @path_templates[template.path]
80
+ context.copy_settings_to(template.context)
81
+ context.copy_settings_to(path.context)
82
+
83
+ yield path.render(renderer), template.render(renderer)
84
+ end
85
+ end
86
+
87
+ def associate_partials
88
+ partials = []
89
+ tsort_each do |template|
90
+ partials.each do |partial|
91
+ template.partial_context(partial)
92
+ end
93
+ if is_partial?(template)
94
+ partials << template
95
+ end
96
+ end
97
+ end
98
+
99
+ def build_context
100
+ tsort_each do |template|
101
+ context_class.absorb_context(template.context_class)
102
+ end
103
+ @path_templates.each_value do |template|
104
+ context_class.absorb_context(template.context_class)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,58 @@
1
+ require 'diecut/configurable'
2
+ require 'diecut/template-reducer'
3
+
4
+ module Diecut
5
+ class Template
6
+ def initialize(path, template_string)
7
+ @path = path
8
+ @template_string = template_string
9
+ @reduced = nil
10
+ @context_class = nil
11
+ @context = nil
12
+ end
13
+
14
+ attr_reader :path, :template_string
15
+
16
+ def partial_context(other)
17
+ reduced.partials.each do |path, nesting|
18
+ next unless path == other.path
19
+ add_subcontext(nesting, other.context_class)
20
+ end
21
+ end
22
+
23
+ def add_subcontext(nesting, other)
24
+ other.field_names.each do |field|
25
+ context_class.build_setting(nesting + [field])
26
+ end
27
+ end
28
+
29
+ def context_class
30
+ @context_class ||= build_context_class
31
+ end
32
+
33
+ def reduced
34
+ @reduced ||= TemplateReducer.new(Mustache::Parser.new.compile(template_string))
35
+ end
36
+
37
+ def partials
38
+ reduced.partials
39
+ end
40
+
41
+ def build_context_class
42
+ klass = Configurable.build_subclass(path)
43
+
44
+ reduced.leaf_fields.each do |field|
45
+ klass.build_setting(field, reduced.sections.include?(field))
46
+ end
47
+ klass
48
+ end
49
+
50
+ def context
51
+ @context ||= context_class.new
52
+ end
53
+
54
+ def render(renderer)
55
+ renderer.render(template_string, context.to_hash)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,71 @@
1
+ module Diecut
2
+ class UIApplier
3
+ attr_accessor :plugins, :ui, :context
4
+
5
+ # setup default values on ui
6
+ # setup dynamic defaults on context
7
+ # copy ui settings to context
8
+ # resolve context config
9
+ # confirm required
10
+ def apply
11
+ check_ui
12
+ basic_defaults
13
+ dynamic_defaults
14
+ copy_to_context
15
+ resolve_context
16
+ confirm_required
17
+ end
18
+
19
+ def check_ui
20
+ ui.check_required
21
+ end
22
+
23
+ def basic_defaults
24
+ context.setup_defaults
25
+ end
26
+
27
+ def dynamic_defaults
28
+ plugins.each do |plugin|
29
+ plugin.context_defaults.each do |default|
30
+ apply_dynamic_default(default)
31
+ end
32
+ end
33
+ end
34
+
35
+ def copy_to_context
36
+ plugins.each do |plugin|
37
+ plugin.options.each do |option|
38
+ copy_option(option)
39
+ end
40
+ end
41
+ end
42
+
43
+ def resolve_context
44
+ plugins.each do |plugin|
45
+ unless plugin.resolve_block.nil?
46
+ plugin.apply_resolve(ui, context)
47
+ end
48
+ end
49
+ end
50
+
51
+ def confirm_required
52
+ context.check_required
53
+ end
54
+
55
+ def apply_dynamic_default(default)
56
+ return if default.simple?
57
+
58
+ segment = context.walk_path(default.context_path).last
59
+
60
+ segment.value = default.compute_value(context)
61
+ end
62
+
63
+ def copy_option(option)
64
+ return unless option.has_context_path?
65
+
66
+ segment = context.walk_path(option.context_path).last
67
+
68
+ segment.value = ui.get_value(option.name.to_sym)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ require 'diecut/configurable'
2
+ module Diecut
3
+ class UIConfig < Configurable
4
+ class << self
5
+ def options_hash
6
+ @options_hash ||= {}
7
+ end
8
+
9
+ def description(name)
10
+ @options_hash.fetch(name).description
11
+ end
12
+
13
+ def required?(name)
14
+ field_metadata(name).is?(:required)
15
+ end
16
+
17
+ def default_for(name)
18
+ field_metadata(name).default_value
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ super
24
+ setup_defaults
25
+ end
26
+
27
+ def get_value(name)
28
+ self.class.field_metadata(name).value_on(self)
29
+ end
30
+ end
31
+ end
data/lib/diecut.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'diecut/mediator'
2
+ require 'diecut/plugin-description'
3
+ require 'diecut/plugin-loader'
4
+
5
+ module Diecut
6
+ class << self
7
+ def plugin_loader
8
+ @plugin_loader ||= PluginLoader.new
9
+ end
10
+
11
+ def plugin_loader=(loader)
12
+ @plugin_loader = loader
13
+ end
14
+
15
+ def clear_plugins
16
+ @plugin_loader = nil
17
+ end
18
+
19
+ def load_plugins(prerelease = false)
20
+ plugin_loader.load_plugins(prerelease)
21
+ end
22
+
23
+ def plugins
24
+ plugin_loader.plugins
25
+ end
26
+
27
+ # Used in a `diecut_plugin.rb` file (either in the `lib/` of a gem, or at
28
+ # the local `~/.config/diecut/diecut_plugin.rb` to register a new plugin.
29
+ #
30
+ # @param name [String, Symbol]
31
+ # Names the plugin so that it can be toggled later
32
+ #
33
+ # @yieldparam description [PluginDescription]
34
+ # The description object to configure the plugin with.
35
+ def plugin(name, &block)
36
+ plugin_loader.describe_plugin(name, &block)
37
+ end
38
+
39
+ def kinds
40
+ plugins.reduce([]) do |list, plugin|
41
+ list + plugin.kinds
42
+ end.uniq
43
+ end
44
+
45
+ def mediator(kind)
46
+ Mediator.new.tap do |med|
47
+ plugins.each do |plug|
48
+ next unless plug.has_kind?(kind)
49
+ med.add_plugin(plug)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1 @@
1
+ require 'diecut/cli'
@@ -0,0 +1,20 @@
1
+ require 'diecut/configurable'
2
+
3
+ describe Diecut::Configurable do
4
+ let :subclass do
5
+ Class.new(described_class){
6
+ setting :shallow
7
+ setting :deeply, Class.new(Diecut::Configurable){
8
+ setting :nested, Class.new(Diecut::Configurable){
9
+ setting :field
10
+ }
11
+ }
12
+ }.tap do |subclass|
13
+ subclass.target_name = "for something"
14
+ end
15
+ end
16
+
17
+ it "inspects nicely" do
18
+ expect(subclass.inspect).to match(/Configurable.*something.*deeply\.nested\.field/)
19
+ end
20
+ end
@@ -0,0 +1,132 @@
1
+ require 'diecut/mill'
2
+ require 'diecut/linter'
3
+
4
+ describe Diecut::Mill do
5
+ let :mill do
6
+ Diecut::Mill.new("kind").tap do |mill|
7
+ mill.valise = valise
8
+ end
9
+ end
10
+
11
+ let :valise do
12
+ Valise::Set.define do
13
+ defaults do
14
+ file "{{testing}}.txt", "I am a {{thing}} for {{testing}}"
15
+ end
16
+ end
17
+ end
18
+
19
+ let :plugin do
20
+ Diecut::PluginDescription.new('dummy', 'dummy.rb').tap do |plugin|
21
+ plugin.option('testing') do |opt|
22
+ opt.goes_to('testing')
23
+ end
24
+ plugin.option('thing') do |opt|
25
+ opt.goes_to(['thing'])
26
+ end
27
+ plugin.default('thing', 15)
28
+ end
29
+ end
30
+
31
+ let :option_colision_plugin do
32
+ Diecut::PluginDescription.new('icky', 'icky.rb').tap do |plugin|
33
+ plugin.default_off
34
+ plugin.option('context_colision') do |opt|
35
+ opt.goes_to('testing')
36
+ end
37
+ end
38
+ end
39
+
40
+ let :default_override_plugin do
41
+ Diecut::PluginDescription.new('stinky', 'stinky.rb').tap do |plugin|
42
+ plugin.default_off
43
+ plugin.default('thing', 'fifteen')
44
+ end
45
+ end
46
+
47
+ let :plugins do
48
+ [plugin, option_colision_plugin, default_override_plugin]
49
+ end
50
+
51
+ let :loader do
52
+ instance_double("Diecut::PluginLoader").tap do |loader|
53
+ allow(loader).to receive(:strict_sequence?).and_return(false)
54
+ allow(loader).to receive(:plugins).and_return(plugins)
55
+ end
56
+ end
57
+
58
+ before :each do
59
+ allow(Diecut).to receive(:plugin_loader).and_return(loader)
60
+
61
+ plugins.each do |plugin|
62
+ mill.mediator.add_plugin(plugin)
63
+ end
64
+ end
65
+
66
+ subject :linter do
67
+ Diecut::Linter.new(mill)
68
+ end
69
+
70
+ let :report do
71
+ linter.report
72
+ end
73
+
74
+ describe "happy set of plugins" do
75
+ it "should produce a report" do
76
+ expect(report).to match(/Total QA failing reports: 0/)
77
+ end
78
+ end
79
+
80
+ describe "with an option collision" do
81
+ before :each do
82
+ mill.mediator.activate('icky')
83
+ end
84
+
85
+ it "should produce a report" do
86
+ expect(report).to match(/Option collisions: FAIL/)
87
+ expect(report).to match(/Total QA failing reports:/)
88
+ expect(report).to match(/there's/)
89
+ end
90
+ end
91
+
92
+ describe "with a missing context field" do
93
+ before :each do
94
+ mill.mediator.activate('icky')
95
+ mill.mediator.deactivate('dummy')
96
+ end
97
+
98
+ it "should produce a report" do
99
+ expect(report).to match(/Template fields all have settings: WARN/)
100
+ expect(report).to match(/Output field\s+Source file/)
101
+ expect(report).to match(/thing\s+{{testing}}.txt/)
102
+ expect(report).not_to match(/^\s*testing\b/)
103
+ expect(report).to match(/Total QA failing reports:/)
104
+ end
105
+ end
106
+
107
+ describe "with intentional override of default" do
108
+ before :each do
109
+ mill.mediator.activate("dummy")
110
+ mill.mediator.activate("stinky")
111
+ allow(loader).to receive(:strict_sequence?).with(plugin, default_override_plugin).and_return(true)
112
+ end
113
+
114
+ it "should produce a report" do
115
+ expect(report).to match(/Overridden context defaults: OK/)
116
+ end
117
+ end
118
+
119
+ describe "with accidental override of default" do
120
+ before :each do
121
+ mill.mediator.activate("dummy")
122
+ mill.mediator.activate("stinky")
123
+ end
124
+
125
+ it "should produce a report" do
126
+ expect(report).to match(/Overridden context defaults: FAIL/)
127
+ expect(report).to match(/Output field\s+Default value\s+Source plugin/)
128
+ expect(report).to match(/thing\s+15\s+dummy/)
129
+ expect(report).to match(/Total QA failing reports:/)
130
+ end
131
+ end
132
+ end
data/spec/mill_spec.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'diecut/mill'
2
+
3
+ describe Diecut::Mill do
4
+ subject :mill do
5
+ Diecut::Mill.new("kind").tap do |mill|
6
+ mill.valise = valise
7
+ end
8
+ end
9
+
10
+ let :valise do
11
+ Valise::Set.define do
12
+ defaults do
13
+ file "{{testing}}.txt", "I am a {{thing}} for {{testing}}"
14
+ end
15
+ end
16
+ end
17
+
18
+ let :plugin do
19
+ Diecut::PluginDescription.new('dummy', 'dummy.rb').tap do |plugin|
20
+ plugin.option('testing') do |opt|
21
+ opt.goes_to('testing')
22
+ end
23
+ plugin.option('thing') do |opt|
24
+ opt.goes_to(['thing'])
25
+ end
26
+ end
27
+ end
28
+
29
+ let :other_plugin do
30
+ Diecut::PluginDescription.new('icky', 'icky.rb')
31
+ end
32
+
33
+ before :each do
34
+ mill.mediator.add_plugin(plugin)
35
+ mill.mediator.add_plugin(other_plugin)
36
+ end
37
+
38
+ it "should render files" do
39
+ mill.activate_plugins do |name|
40
+ name == 'dummy'
41
+ end
42
+ ui = mill.user_interface
43
+
44
+ ui.testing = "checking"
45
+ ui.thing = "test file"
46
+
47
+ mill.churn(ui) do |path, contents|
48
+ expect(path).to eq "checking.txt"
49
+ expect(contents).to eq "I am a test file for checking"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,109 @@
1
+ require 'diecut/plugin-loader'
2
+
3
+ describe Diecut::PluginLoader do
4
+ let :root_gem do
5
+ instance_double('Gem::Specification', "root", :name => 'root', :matches_for_glob => ['/root_path/diecut_plugin.rb'],
6
+ :dependencies => [])
7
+ end
8
+
9
+ let :simple_dep do
10
+ instance_double('Gem::Specification', 'simple', :name => 'simple', :matches_for_glob => ['/simple_path/diecut_plugin.rb'],
11
+ :dependencies => [instance_double("Gem::Dependency", :name => "root")] )
12
+ end
13
+
14
+ let :side_dep do
15
+ instance_double('Gem::Specification', 'side', :name => 'side', :matches_for_glob => ['/side_path/diecut_plugin.rb'],
16
+ :dependencies => [
17
+ instance_double("Gem::Dependency", :name => "root"),
18
+ instance_double("Gem::Dependency", :name => "simple"),
19
+ instance_double("Gem::Dependency", :name => "cycle")
20
+ ])
21
+ end
22
+
23
+ let :cycle_dep do
24
+ instance_double('Gem::Specification', 'cycle', :name => 'cycle', :matches_for_glob => ['/cycle_path/diecut_plugin.rb'],
25
+ :dependencies =>[ instance_double("Gem::Dependency", :name => "side") ])
26
+ end
27
+
28
+ let :gem_specs do
29
+ [root_gem, simple_dep, side_dep, cycle_dep]
30
+ end
31
+
32
+ let :plugin_defs do
33
+ {
34
+ "/root_path/diecut_plugin.rb" => proc{
35
+ loader.describe_plugin("root"){}
36
+ },
37
+ "/simple_path/diecut_plugin.rb" => proc{
38
+ loader.describe_plugin("simple"){}
39
+ },
40
+ "/side_path/diecut_plugin.rb" => proc{
41
+ loader.describe_plugin("side"){}
42
+ },
43
+ "/cycle_path/diecut_plugin.rb" => proc{
44
+ loader.describe_plugin("cycle"){}
45
+ },
46
+ "<DEFAULTS>:diecut_plugin.rb" => proc{
47
+ loader.describe_plugin("local"){}
48
+ }
49
+ }
50
+ end
51
+
52
+ let :valise do
53
+ Valise::Set.define do
54
+ defaults do
55
+ file "diecut_plugin.rb", "I am a thing for testing"
56
+ end
57
+ end
58
+ end
59
+
60
+ subject :loader do
61
+ Diecut::PluginLoader.new.tap do |loader|
62
+ loader.local_valise = valise
63
+ current_caller = "no clue"
64
+ allow(loader).to receive(:caller_locations){ [current_caller] }
65
+ allow(loader).to receive(:latest_specs).and_return(gem_specs)
66
+ allow(loader).to receive(:require_plugin) {|path|
67
+ current_caller = double("Location", :absolute_path => path)
68
+ plugin_defs.fetch(path).call
69
+ }
70
+ end
71
+ end
72
+
73
+ let :root_plugin do
74
+ loader.plugins.find{|pl| pl.name == 'root' }
75
+ end
76
+
77
+ let :simple_plugin do
78
+ loader.plugins.find{|pl| pl.name == 'simple' }
79
+ end
80
+
81
+ let :side_plugin do
82
+ loader.plugins.find{|pl| pl.name == 'side' }
83
+ end
84
+
85
+ let :cycle_plugin do
86
+ loader.plugins.find{|pl| pl.name == 'cycle' }
87
+ end
88
+
89
+ let :local_plugin do
90
+ loader.plugins.find{|pl| pl.name == 'local' }
91
+ end
92
+
93
+ before :each do
94
+ loader.load_plugins
95
+ end
96
+
97
+
98
+ it "should load some plugins" do
99
+ expect(loader.plugins.length).to eq(5)
100
+ end
101
+
102
+ it "should trace sequencing" do
103
+ expect(loader.strict_sequence?(root_plugin, local_plugin)).to eq(true)
104
+ expect(loader.strict_sequence?(local_plugin, root_plugin)).to eq(false)
105
+ expect(loader.strict_sequence?(root_plugin, simple_plugin)).to eq(true)
106
+ expect(loader.strict_sequence?(root_plugin, cycle_plugin)).to eq(true)
107
+ expect(loader.strict_sequence?(side_plugin, cycle_plugin)).to eq(true)
108
+ end
109
+ end