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.
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
@@ -0,0 +1,10 @@
1
+ $: << 'lib'
2
+
3
+ require 'diecut'
4
+ require 'diecut/cli'
5
+
6
+ Diecut.load_plugins
7
+ Diecut.kinds.each do |kind|
8
+ Diecut::CommandLine.add_kind(kind)
9
+ end
10
+ Diecut::CommandLine.start
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
@@ -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