diecut 0.0.1

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.
@@ -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