diecut 0.0.1

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