diecut 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b476ce0ef0d2ba317287aa92d5b92c641b586ed5
4
- data.tar.gz: d3249cb202a081d0531a1015e829cd347f50ce06
3
+ metadata.gz: 107febe45026dd5f8483a22c823ff7df081d9376
4
+ data.tar.gz: 3ad4f743f9aaa20bbe24a5c72a31d4c1ef813048
5
5
  SHA512:
6
- metadata.gz: e1ee0f0789205ffad28377709c60ec1aa54862c5db09bc07cbc15ebd65f7187235fc87bd3ac3ecf78c299cd4f4ebd3efcb84dc4aba31ee13c62c7e225bb44ffb
7
- data.tar.gz: 045e339e9a7c88f8a22a8741b5bf24ddab0f3018d9b20c2efeb9855a622ab92917a49dde445367176e30ddf2be93069d66e83d2ef0a57f4ac898c0b405ec2603
6
+ metadata.gz: 4d6837550e1a368a23781bfdd8c0b66450ad3ed19f6d831102cc9b4541285dbdbe4cf45010fa7b767a3babdc29fb78ddff405dfd89d5c5cfa289a5dd6903549f
7
+ data.tar.gz: 450068202354f5fe74a57f309beeaeb08e11c2eceb4adeb74ca8083d2570679333567d449b616e750146cbf06041d1f363e22f8945b955dff4c5db940c0d4b4d
data/bin/diecut CHANGED
File without changes
data/lib/diecut/cli.rb CHANGED
@@ -3,37 +3,81 @@ require 'diecut'
3
3
  require 'diecut/mill'
4
4
 
5
5
  module Diecut
6
- class KindCli < Thor
7
- include Thor::Actions
6
+ module Cli
7
+ class KindGroup < Thor::Group
8
+ def self.subclass_for(kind, mediator = nil, example_ui = nil)
9
+ mediator ||= Diecut.mediator(kind)
10
+ example_ui ||= mediator.build_example_ui
8
11
 
9
- desc "generate TARGET", "Generate code"
10
- def generate(target_dir)
11
- self.destination_root = target_dir
12
+ Class.new(self) do
13
+ def self.kind
14
+ @kind
15
+ end
12
16
 
13
- mill = Mill.new(self.class.kind)
14
- mill.activate_plugins {|name| options["with-#{name}"] }
17
+ mediator.plugins.each do |plugin|
18
+ class_option "with-#{plugin.name}", :default => plugin.default_active?
19
+ end
15
20
 
16
- ui = mill.user_interface
17
- options.delete_if{|_, value| value.nil?}
18
- ui.from_hash(options)
21
+ setup_subclass(mediator, example_ui)
22
+ end.tap do |klass|
23
+ klass.instance_variable_set("@kind", kind)
24
+ end
25
+ end
19
26
 
20
- mill.churn(ui) do |path, contents|
21
- create_file(path, contents)
27
+ def self.setup_subclass(mediator, example_ui)
22
28
  end
23
29
  end
24
30
 
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
31
+ class Generate < KindGroup
32
+ include Thor::Actions
33
+
34
+ def self.setup_subclass(mediator, example_ui)
35
+ example_ui.field_names.each do |field|
36
+ class_option(field, {
37
+ :desc => example_ui.description(field) || field,
38
+ :required => example_ui.required?(field),
39
+ :default => example_ui.default_for(field)
40
+ })
41
+ end
42
+ end
43
+
44
+ def files
45
+ self.destination_root = target_dir
46
+
47
+ mill = Mill.new(self.class.kind)
33
48
  mill.activate_plugins {|name| options["with-#{name}"] }
49
+
50
+ ui = mill.user_interface
51
+ options.delete_if{|_, value| value.nil?}
52
+ ui.from_hash(options)
53
+
54
+ mill.churn(ui) do |path, contents|
55
+ create_file(path, contents)
56
+ end
34
57
  end
58
+ end
35
59
 
36
- puts Linter.new(mill).report
60
+ class TargetedGenerate < Generate
61
+ argument :target_dir, :required => true, :type => :string, :banner => "The directory to use as the root of generated output"
62
+ end
63
+
64
+ class Lint < KindGroup
65
+ class_option :all_on, :default => false, :type => :boolean
66
+
67
+ def lint
68
+ require 'diecut/linter'
69
+ require 'diecut/error-report'
70
+ mill = Mill.new(self.class.kind)
71
+ Diecut.issue_handler = Diecut::ErrorHandling::Reporter.new(mill)
72
+
73
+ if options["all_on"]
74
+ mill.activate_plugins{ true }
75
+ else
76
+ mill.activate_plugins{|name| options["with-#{name}"] }
77
+ end
78
+
79
+ puts Linter.new(mill).report
80
+ end
37
81
  end
38
82
  end
39
83
 
@@ -42,29 +86,15 @@ module Diecut
42
86
  mediator = Diecut.mediator(plugin_kind)
43
87
  example_ui = mediator.build_example_ui
44
88
 
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
89
+ Class.new(Thor) do
90
+ gen = Cli::TargetedGenerate.subclass_for(plugin_kind, mediator, example_ui)
91
+ method_options(gen.class_options)
92
+ register gen, "generate", "#{plugin_kind} generate TARGET", "Generate #{plugin_kind} output"
58
93
 
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
94
+ lint = Cli::Lint.subclass_for(plugin_kind, mediator, example_ui)
95
+ method_options(lint.class_options)
96
+ register lint, "lint", "#{plugin_kind} lint", "Check well-formed-ness of #{plugin_kind} code generators"
63
97
  end
64
-
65
- klass.kind(plugin_kind)
66
-
67
- klass
68
98
  end
69
99
 
70
100
  def self.add_kind(kind)
@@ -1,4 +1,6 @@
1
1
  require 'calibrate'
2
+ require 'diecut/errors'
3
+
2
4
  module Diecut
3
5
  class Configurable
4
6
  include Calibrate::Configurable
@@ -58,14 +60,14 @@ module Diecut
58
60
  if from_value.is_a?(Class) and from_value < Calibrate::Configurable
59
61
  into_value.absorb_context(from_value)
60
62
  else
61
- raise "Field clash: #{name.inspect} is already a complex value, but a simple value in the absorbed configurable"
63
+ raise FieldClash, "#{name.inspect} is already a complex value, but a simple value in the absorbed configurable"
62
64
  end
63
65
  else
64
66
  unless from_value.is_a?(Class) and from_value < Calibrate::Configurable
65
67
  # Noop - maybe should compare the default values? - should always
66
68
  # be nil right now...
67
69
  else
68
- raise "Field clash: #{name.inspect} is already a simple value, but a complex value on the absorbed configurable"
70
+ raise FieldClash, "#{name.inspect} is already a simple value, but a complex value on the absorbed configurable"
69
71
  end
70
72
  end
71
73
  end
@@ -3,12 +3,17 @@ module Diecut
3
3
  class ContextHandler
4
4
  attr_accessor :context_class, :ui_class, :plugins
5
5
 
6
+ def issue_handler
7
+ @issue_handler ||= Diecut.issue_handler
8
+ end
9
+ attr_writer :issue_handler
10
+
6
11
  def apply_simple_defaults
7
12
  plugins.each do |plugin|
8
13
  plugin.context_defaults.each do |default|
9
14
  next unless default.simple?
10
15
  begin
11
- apply_simple_default(default)
16
+ apply_simple_default(plugin, default)
12
17
  rescue Error
13
18
  raise Error, "Plugin #{plugin.name.inspect} failed"
14
19
  end
@@ -19,7 +24,7 @@ module Diecut
19
24
  def apply_to_ui
20
25
  plugins.each do |plugin|
21
26
  plugin.options.each do |option|
22
- apply_option_to_ui(option)
27
+ apply_option_to_ui(plugin, option)
23
28
  end
24
29
  end
25
30
  end
@@ -43,21 +48,25 @@ module Diecut
43
48
  end
44
49
  end
45
50
 
46
- def apply_simple_default(default)
51
+ def apply_simple_default(plugin, default)
47
52
  target = context_class.walk_path(default.context_path).last
48
53
  if target.metadata.nil?
49
- raise UnusedDefault, "No template uses a value at #{default.context_path.inspect}"
54
+ issue_handler.unused_default(plugin.name, context_path)
50
55
  else
51
56
  target.metadata.default_value = default.value
52
57
  target.metadata.is(:defaulting)
53
58
  end
54
59
  end
55
60
 
56
- def apply_option_to_ui(option)
61
+ def apply_option_to_ui(plugin, option)
57
62
  ui_class.options_hash[option.name] = option
58
63
 
59
64
  if option.has_context_path?
60
65
  context_metadata = context_class.walk_path(option.context_path).last.metadata
66
+ if context_metadata.nil?
67
+ issue_handler.missing_context_field(plugin.name, option.name, option.context_path)
68
+ return
69
+ end
61
70
  if option.has_default?
62
71
  ui_class.setting(option.name, option.default_value)
63
72
  elsif context_metadata.is?(:defaulting)
@@ -0,0 +1,58 @@
1
+ require 'diecut/errors'
2
+ require 'diecut/report'
3
+ require 'diecut/report-builders/missing-context-field'
4
+ require 'diecut/report-builders/unused-default'
5
+ require 'diecut/report-builders/invalid-plugin'
6
+ require 'diecut/report-builders/exception-report'
7
+
8
+ module Diecut
9
+ module ErrorHandling
10
+ class Reporter < Base
11
+ def initialize(mill)
12
+ @mill = mill
13
+ end
14
+
15
+ def missing_context_field_report
16
+ @missing_context_field_report ||= ReportBuilders::MissingContextField.new(@mill)
17
+ end
18
+
19
+ def unused_default_report
20
+ @unused_default_report ||= ReportBuilders::UnusedDefault.new(@mill)
21
+ end
22
+
23
+ def invalid_plugin_report
24
+ @invalid_plugin_report ||= ReportBuilders::InvalidPlugin.new(@mill)
25
+ end
26
+
27
+ def exception_report
28
+ @exception_report ||= ReportBuilders::Exceptions.new(@mill)
29
+ end
30
+
31
+ def missing_context_field(plugin_name, option_name, context_path)
32
+ missing_context_field_report.add(option_name, context_path, plugin_name)
33
+ end
34
+
35
+ def unused_default(plugin_name, context_path)
36
+ unused_default_report.add(context_path, plugin_name)
37
+ end
38
+
39
+ def invalid_plugin(name, context_path, value)
40
+ invalid_plugin_report.add(name, context_path, value)
41
+ end
42
+
43
+ def handle_exception(ex)
44
+ raise unless ex.is_a? Diecut::Error
45
+ exception_report.add(ex.class.name, ex.message, (ex.backtrace || [""]).first)
46
+ end
47
+
48
+ def reports
49
+ [
50
+ missing_context_field_report.go,
51
+ unused_default_report.go,
52
+ invalid_plugin_report.go,
53
+ exception_report.go
54
+ ]
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/diecut/errors.rb CHANGED
@@ -1,4 +1,71 @@
1
1
  module Diecut
2
+ module ErrorHandling
3
+ class Base
4
+ def missing_context_field_message(option_name, context_path)
5
+ "No template uses a value at #{context_path.inspect}, provided by #{option_name}"
6
+ end
7
+
8
+ def unused_default_message(context_path)
9
+ "No template uses a value at #{context_path.inspect}, provided as a default"
10
+ end
11
+
12
+ def invalid_plugin_message(name, context_path, value)
13
+ "Default on #{context_path.inspect} from plugin #{name} has both a simple default value (#{value}) and a dynamic block value, which isn't allowed."
14
+ end
15
+
16
+ def missing_context_field(plugin_name, option_name, context_path)
17
+ issue(missing_context_field_message(option_name, context_path))
18
+ end
19
+
20
+ def unused_default(plugin_name, context_path)
21
+ issue(usused_default_message(context_path))
22
+ end
23
+
24
+ def invalid_plugin(name, context_path, value)
25
+ issue invalid_plugin_message(name, context_path, value)
26
+ end
27
+
28
+ def handle_exception(ex)
29
+ if Error === ex
30
+ issue(ex.message)
31
+ else
32
+ raise
33
+ end
34
+ end
35
+ end
36
+
37
+ class Silent < Base
38
+ def issue(message)
39
+ end
40
+ end
41
+
42
+ class AllWarn < Base
43
+ def issue(message)
44
+ warn message
45
+ end
46
+ end
47
+
48
+ class FatalRaise < AllWarn
49
+ def invalid_plugin(name, context_path, value)
50
+ raise InvalidPlugin, invalid_plugin_message(name, context_path, value)
51
+ end
52
+ end
53
+
54
+ class AllRaise < FatalRaise
55
+ def handle_exception(ex)
56
+ raise
57
+ end
58
+
59
+ def missing_context_field(option_name, context_path)
60
+ raise MissingContext, missing_context_field_message(option_name, context_path)
61
+ end
62
+
63
+ def unused_default(context_path)
64
+ raise UnusedDefault, unused_default_message(context_path)
65
+ end
66
+ end
67
+ end
68
+
2
69
  class Error < RuntimeError;
3
70
  def message
4
71
  if cause.nil?
@@ -9,6 +76,8 @@ module Diecut
9
76
  end
10
77
  end
11
78
  class UnusedDefault < Error; end
79
+ class MissingContext < Error; end
12
80
  class OverriddenDefault < Error; end
13
- class InvalidConfig < Error; end
81
+ class InvalidPlugin < Error; end
82
+ class FieldClash < Error; end
14
83
  end
data/lib/diecut/linter.rb CHANGED
@@ -1,6 +1,12 @@
1
- require 'diecut/report'
1
+ require 'diecut/report-builder'
2
+
3
+ require 'diecut/report-builders/template-list'
4
+ require 'diecut/report-builders/overridden-context-defaults'
5
+ require 'diecut/report-builders/option-collision'
6
+ require 'diecut/report-builders/orphaned-field'
2
7
 
3
8
  module Diecut
9
+
4
10
  class Linter
5
11
  def initialize(mill)
6
12
  @mill = mill
@@ -10,12 +16,13 @@ module Diecut
10
16
  def report
11
17
  @ui = mill.user_interface
12
18
 
13
- formatter = ReportFormatter.new([
14
- option_collision_report,
15
- orphaned_fields,
16
- overridden_context_defaults
17
- ])
18
-
19
+ reports = ReportBuilder.all_kinds.map do |kind|
20
+ kind.new(mill).go
21
+ end
22
+ if Diecut.issue_handler.respond_to?(:reports)
23
+ reports += Diecut.issue_handler.reports
24
+ end
25
+ formatter = ReportFormatter.new( reports)
19
26
  formatter.to_s
20
27
  end
21
28
 
@@ -49,129 +56,5 @@ module Diecut
49
56
  end
50
57
  end
51
58
  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
59
  end
177
60
  end
@@ -40,10 +40,10 @@ module Diecut
40
40
  # < User gets involved >
41
41
  #
42
42
  def build_example_ui
43
- ui_class = Class.new(UIConfig)
43
+ ui_class = UIConfig.build_subclass("Example UI")
44
44
 
45
45
  handler = ContextHandler.new
46
- handler.context_class = Class.new(Configurable)
46
+ handler.context_class = Configurable.build_subclass("dummy context")
47
47
  handler.ui_class = ui_class
48
48
  handler.plugins = @plugins
49
49
 
@@ -54,7 +54,7 @@ module Diecut
54
54
  end
55
55
 
56
56
  def build_ui_class(context_class)
57
- ui_class = Class.new(UIConfig)
57
+ ui_class = UIConfig.build_subclass("User Interface")
58
58
 
59
59
  handler = ContextHandler.new
60
60
  handler.context_class = context_class
data/lib/diecut/mill.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'valise'
2
2
  require 'diecut'
3
+ require 'diecut/errors'
3
4
  require 'diecut/template-set'
4
5
 
5
6
  module Diecut
@@ -8,7 +9,7 @@ module Diecut
8
9
  @kind = kind
9
10
  end
10
11
  attr_reader :kind
11
- attr_writer :valise, :mediator, :templates
12
+ attr_writer :valise, :mediator, :templates, :issue_handler
12
13
 
13
14
  def mediator
14
15
  @mediator ||= Diecut.mediator(kind)
@@ -29,12 +30,17 @@ module Diecut
29
30
  end
30
31
 
31
32
  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)
33
+ @valise ||=
34
+ begin
35
+ stems = mediator.activated_plugins.map do |plugin|
36
+ plugin.stem_for(kind)
37
+ end
38
+ Valise::Set.define do
39
+ stems.each do |stem|
40
+ ro stem.template_dir
41
+ end
42
+ end
43
+ end
38
44
  end
39
45
 
40
46
  def load_files
@@ -22,6 +22,11 @@ module Diecut
22
22
  attr_reader :default_activated, :name, :source_path, :context_defaults,
23
23
  :options, :resolve_block
24
24
 
25
+ def issue_handler
26
+ @issue_handler ||= Diecut.issue_handler
27
+ end
28
+ attr_writer :issue_handler
29
+
25
30
  def kinds
26
31
  @kind_stems.keys
27
32
  end
@@ -67,7 +72,7 @@ module Diecut
67
72
  #
68
73
  def for_kind(kind, templates = nil, stem = nil)
69
74
  stem ||= [kind]
70
- templates ||= "templates"
75
+ templates ||= "diecut_templates"
71
76
  templates = File.expand_path(templates, File.dirname(caller_locations(1..1).first.absolute_path))
72
77
  @kind_stems[kind] = KindStem.new(kind, stem, templates)
73
78
  end
@@ -112,6 +117,7 @@ module Diecut
112
117
  [context_path]
113
118
  end
114
119
  if value != NO_VALUE and not block.nil?
120
+ issue_handler.invalid_plugin(name, context_path, value)
115
121
  raise InvalidPlugin, "Default on #{name.inspect} both has a simple default value (#{value}) and a dynamic block value, which isn't allowed."
116
122
  end
117
123
  @context_defaults << ContextDefault.new(context_path, value, block)
@@ -0,0 +1,114 @@
1
+ require 'diecut/report'
2
+
3
+ module Diecut
4
+ class ReportBuilder
5
+ def self.all_kinds
6
+ @all_kinds ||= []
7
+ end
8
+
9
+ def self.register
10
+ ReportBuilder.all_kinds << self
11
+ end
12
+
13
+ def initialize(mill)
14
+ @mill = mill
15
+ end
16
+ attr_reader :mill
17
+
18
+ def unindent(text)
19
+ return if text.nil?
20
+ indent = text.scan(/(^[ \t]*)\S/).map{|cap| cap.first}.max_by(&:length)
21
+ text.gsub(%r{^#{indent}},'')
22
+ end
23
+
24
+ def each_plugin
25
+ mill.mediator.activated_plugins.each do |plugin|
26
+ yield plugin
27
+ end
28
+ end
29
+
30
+ def each_default
31
+ each_plugin do |plugin|
32
+ plugin.context_defaults.each do |default|
33
+ yield default, plugin
34
+ end
35
+ end
36
+ end
37
+
38
+ def each_option
39
+ each_plugin do |plugin|
40
+ plugin.options.each do |option|
41
+ yield option, plugin
42
+ end
43
+ end
44
+ end
45
+
46
+ def each_template
47
+ mill.templates.templates.each do |name, template|
48
+ yield name, template
49
+ end
50
+ end
51
+
52
+ def report
53
+ @report ||= build_report
54
+ end
55
+
56
+ def build_report
57
+ Report.new(report_name, report_fields)
58
+ end
59
+
60
+ def strict_sequence?(first, second)
61
+ return false if first == second
62
+ Diecut.plugin_loader.strict_sequence?(first, second)
63
+ end
64
+
65
+ def go
66
+ collect
67
+ review
68
+ report
69
+ end
70
+
71
+ def report_status
72
+ report.empty? ? 'OK' : 'FAIL'
73
+ end
74
+
75
+ def pass_summary
76
+ nil
77
+ end
78
+
79
+ def fail_summary
80
+ nil
81
+ end
82
+
83
+ def other_summary
84
+ nil
85
+ end
86
+
87
+ def pass_advice
88
+ nil
89
+ end
90
+
91
+ def fail_advice
92
+ nil
93
+ end
94
+
95
+ def other_advice
96
+ nil
97
+ end
98
+
99
+ def review
100
+ report.status = report_status.to_s.upcase
101
+ case report.status
102
+ when "OK", "PASS"
103
+ report.summary = pass_summary
104
+ report.advice = unindent(pass_advice)
105
+ when 'FAIL'
106
+ report.summary = fail_summary
107
+ report.advice = unindent(fail_advice)
108
+ else
109
+ report.summary = other_summary
110
+ report.advice = unindent(other_advice)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,32 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class Exceptions < ReportBuilder
6
+ def report_name
7
+ "Exceptions raised during definition"
8
+ end
9
+
10
+ def report_fields
11
+ ["Exception class", "message", "source line"]
12
+ end
13
+
14
+ def collect
15
+ end
16
+
17
+ def add(*args)
18
+ report.add(*args)
19
+ end
20
+
21
+ def fail_summary
22
+ "Exceptions were raised during the kind definition process"
23
+ end
24
+
25
+ def other_advice
26
+ <<-EOA
27
+ Exceptions were raised while defining plugins for generation. Those need to be fixed.
28
+ EOA
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class InvalidPlugin < ReportBuilder
6
+ def report_name
7
+ "General plugin health"
8
+ end
9
+
10
+ def report_fields
11
+ ["Plugin name", "Problem description"]
12
+ end
13
+
14
+ def collect
15
+ end
16
+
17
+ def add(*args)
18
+ report.add(*args)
19
+ end
20
+
21
+ def other_summary
22
+ "There were problems defining plugins"
23
+ end
24
+
25
+ def other_advice
26
+ <<-EOA
27
+ The plugins above had unrecoverable issues while being defined. They
28
+ should be fixed, or not included during generation.
29
+ EOA
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class MissingContextField < ReportBuilder
6
+ def report_name
7
+ "Unused options"
8
+ end
9
+
10
+ def report_fields
11
+ ["Output field name", "Option_name", "Plugin name"]
12
+ end
13
+
14
+ def report_status
15
+ report.empty? ? "OK" : "WARN"
16
+ end
17
+
18
+ def collect
19
+ end
20
+
21
+ def add(*args)
22
+ report.add(*args)
23
+ end
24
+
25
+ def other_summary
26
+ "Options provide values that aren't used by any template"
27
+ end
28
+
29
+ def other_advice
30
+ <<-EOA
31
+ Plugins defined options that go to fields that don't appear in templates.
32
+
33
+ It's possible that a plugin defined an option for its templates but they were
34
+ overridden, so the fields disappeared. Diecut doesn't yet check for
35
+ that case. In those cases, you can ignore this warning.
36
+
37
+ The other possiblity is that the default path has a typo. This is
38
+ especially likely if there's also a report about a missing output
39
+ field. The option might be used in a resolve somewhere, so even if it
40
+ doesn't directly set an output field, it might influence generation
41
+ that way.
42
+
43
+ Do be careful to check this option: the option
44
+ EOA
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class OptionCollisions < ReportBuilder
6
+ register
7
+
8
+ def report_name
9
+ "Option collisions"
10
+ end
11
+
12
+ def report_fields
13
+ ["Output target", "Option name", "Source plugin"]
14
+ end
15
+
16
+ def collect
17
+ option_targets = Hash.new{|h,k| h[k]=[]}
18
+ each_option do |option, plugin|
19
+ next unless option.has_context_path?
20
+ option_targets[option.context_path] << [plugin, option]
21
+ end
22
+ option_targets.each_value do |set|
23
+ if set.length > 1
24
+ set.each do |plugin, option|
25
+ report.add(option.context_path.join("."), option.name, plugin.name)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def fail_summary
32
+ "Multiple options assign the same values to be rendered"
33
+ end
34
+
35
+ def fail_advice
36
+ (<<-EOA)
37
+ This is problem because two options in the user interface both change
38
+ rendered values. If a user supplies both with different values, the
39
+ output isn't predictable (either one might take effect).
40
+
41
+ Most likely, this is a simple error: remove options from each group
42
+ that targets the same rendered value until only one remains. It may
43
+ also be that one option has a typo - that there's a rendering target
44
+ that's omitted.
45
+ EOA
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,63 @@
1
+ module Diecut
2
+ module ReportBuilders
3
+ class OrphanedField < ReportBuilder
4
+ register
5
+
6
+ def report_name
7
+ "Template fields all have settings"
8
+ end
9
+
10
+ def report_fields
11
+ ["Output field", "Source file"]
12
+ end
13
+
14
+ def collect
15
+ context_class = mill.context_class
16
+
17
+ required_fields = {}
18
+
19
+ context_class.field_names.each do |field_name|
20
+ if context_class.field_metadata(field_name).is?(:required)
21
+ required_fields[field_name.to_s] = []
22
+ end
23
+ end
24
+
25
+ each_template do |name, template|
26
+ template.reduced.leaf_fields.each do |field|
27
+ field = field.join(".")
28
+ if required_fields.has_key?(field)
29
+ required_fields[field] << template.path
30
+ end
31
+ end
32
+ end
33
+
34
+ each_option do |option, plugin|
35
+ next unless option.has_context_path?
36
+ field = option.context_path.join(".")
37
+ required_fields.delete(field)
38
+ end
39
+
40
+ required_fields.each do |name, targets|
41
+ targets.each do |target|
42
+ report.add(name, target)
43
+ end
44
+ end
45
+ end
46
+
47
+ def report_status
48
+ report.empty? ? "OK" : "WARN"
49
+ end
50
+
51
+
52
+ def other_advice
53
+ <<-EOA
54
+ These fields might not receive a value during generation, which will
55
+ raise an error at use time.
56
+
57
+ It's possible these fields are set in a resolve block in one of the
58
+ plugins - Diecut can't check for that yet.
59
+ EOA
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,62 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class OverriddenContextDefaults < ReportBuilder
6
+ register
7
+
8
+ def report_name
9
+ "Overridden context defaults"
10
+ end
11
+
12
+ def report_fields
13
+ ["Output field", "Default value", "Source plugin"]
14
+ end
15
+
16
+ def collect
17
+ default_values = Hash.new{|h,k| h[k]=[]}
18
+ each_default do |default, plugin|
19
+ next unless default.simple?
20
+
21
+ default_values[default.context_path] << [default, plugin]
22
+ end
23
+
24
+ default_values.each do |key, set|
25
+ default_values[key] = set.find_all do |plugin|
26
+ !set.any?{|child| strict_sequence?(plugin[1], child[1]) }
27
+ end
28
+ end
29
+
30
+ default_values.each_value do |set|
31
+ if set.length > 1
32
+ set.each do |default, plugin|
33
+
34
+ report.add(default.context_path.join("."), default.value, plugin.name)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def fail_summary
41
+ "Multiple plugins assign different values to be rendered by default"
42
+ end
43
+
44
+ def fail_advice
45
+ (<<-EOA)
46
+ This is a problem because each plugin may be assuming it's default
47
+ value, and since there's no guarantee in which order the plugins are
48
+ loaded, the actual default value is difficult to predict. In general,
49
+ this kind of override behavior can be difficult to reason about.
50
+
51
+ Either the collision is accidental, in which case the default value
52
+ should be removed from one plugin or the other. If the override is
53
+ intentional, then the overriding plugin's gem should depend on the
54
+ overridden one's - since you are overriding the value intentionally,
55
+ it makes sense to ensure that the value is there to override. Diecut
56
+ will load plugins such that the dependant plugins are loaded later,
57
+ which solves the predictability problem.
58
+ EOA
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,38 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ class TemplateListBuilder < ReportBuilder
5
+ register
6
+
7
+ def report_name
8
+ "Templates included"
9
+ end
10
+
11
+ def report_fields
12
+ ["Template path"]
13
+ end
14
+
15
+ def collect
16
+ each_template do |name, template|
17
+ report.add(template.path)
18
+ end
19
+ end
20
+
21
+ def report_status
22
+ report.empty? ? 'FAIL' : 'OK'
23
+ end
24
+
25
+ def fail_summary
26
+ report.summary = "No templates will render"
27
+ end
28
+
29
+ def fail_advice
30
+ (<<-EOA)
31
+ No plugin provides any templates. This is probably simple misconfiguration a plugin, or an important plugin has been omitted.
32
+
33
+ Plugins: #{mill.mediator.activated_plugins.map(&:name)}
34
+ Plugin template paths: #{mill.valise.to_s}
35
+ EOA
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ require 'diecut/report-builder'
2
+
3
+ module Diecut
4
+ module ReportBuilders
5
+ class UnusedDefault < ReportBuilder
6
+ def report_name
7
+ "Defaults are declared but unused"
8
+ end
9
+
10
+ def report_fields
11
+ ["Output field name", "Plugin name"]
12
+ end
13
+
14
+ def report_status
15
+ report.empty? ? "OK" : "WARN"
16
+ end
17
+
18
+ def collect
19
+ end
20
+
21
+ def add(*args)
22
+ report.add(*args)
23
+ end
24
+
25
+ def other_summary
26
+ "Defaults are defined for fields that don't exist in output templates"
27
+ end
28
+
29
+ def other_advice
30
+ <<-EOA
31
+ Plugins defined defaults for fields that don't appear in templates.
32
+
33
+ It's possible that a plugin defined a default but it's template was
34
+ overridden, so the fields disappeared. Diecut doesn't yet check for
35
+ that case. In those cases, you can ignore this warning.
36
+
37
+ The other possiblity is that the default path has a typo. This is
38
+ especially likely if there's also a report about a missing output
39
+ field.
40
+ EOA
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/diecut/report.rb CHANGED
@@ -15,7 +15,7 @@ module Diecut
15
15
 
16
16
  def template
17
17
  (<<-EOT).gsub(/^ /, '')
18
- {{#reports}}{{#status_color}}{{name}}: {{status}} {{#length}} {{length}}{{/length}}
18
+ {{#reports}}{{#status_color}}{{name}} {{status}} {{#length}} {{length}}{{/length}}
19
19
  {{/status_color}}
20
20
  {{#summary}}{{summary}}
21
21
  {{/summary}}{{^empty }}{{#headers}}{{it}} {{/headers}}
@@ -24,6 +24,7 @@ module Diecut
24
24
  {{advice}}
25
25
  {{/advice}}
26
26
  {{/reports}}
27
+
27
28
  {{#status_color}}Total QA report items: {{total_items}}
28
29
  Total QA failing reports: {{total_fails}}
29
30
  {{/status_color}}
@@ -45,6 +46,14 @@ module Diecut
45
46
  reports.inject(0){|sum, report| sum + (report.passed ? 0 : 1)}
46
47
  end
47
48
 
49
+ def report_name_width
50
+ @report_name_width ||= reports.map(&:name).map(&:size).max
51
+ end
52
+
53
+ def sized_name(name)
54
+ array.take(widths.length).zip(widths).map{|item, width| { it: item.to_s.ljust(width)}}
55
+ end
56
+
48
57
  def context(renderer)
49
58
  bad_color = proc{|text,render| Paint[renderer.render(text), :red]}
50
59
  good_color = proc{|text,render| Paint[renderer.render(text), :green]}
@@ -58,6 +67,7 @@ module Diecut
58
67
  status_color: passed? ? good_color : bad_color
59
68
  }
60
69
  context[:reports].each do |report|
70
+ report[:name] = report[:name].rjust(report_name_width)
61
71
  report[:status_color] =
62
72
  case report[:status]
63
73
  when /ok/i
data/lib/diecut.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'diecut/mediator'
2
2
  require 'diecut/plugin-description'
3
3
  require 'diecut/plugin-loader'
4
+ require 'diecut/errors'
4
5
 
5
6
  module Diecut
6
7
  class << self
@@ -24,6 +25,11 @@ module Diecut
24
25
  plugin_loader.plugins
25
26
  end
26
27
 
28
+ def issue_handler
29
+ @issue_handler ||= ErrorHandling::AllWarn.new
30
+ end
31
+ attr_writer :issue_handler
32
+
27
33
  # Used in a `diecut_plugin.rb` file (either in the `lib/` of a gem, or at
28
34
  # the local `~/.config/diecut/diecut_plugin.rb` to register a new plugin.
29
35
  #
data/spec/linter_spec.rb CHANGED
@@ -83,7 +83,7 @@ describe Diecut::Mill do
83
83
  end
84
84
 
85
85
  it "should produce a report" do
86
- expect(report).to match(/Option collisions: FAIL/)
86
+ expect(report).to match(/Option collisions\s*FAIL/)
87
87
  expect(report).to match(/Total QA failing reports:/)
88
88
  expect(report).to match(/there's/)
89
89
  end
@@ -96,7 +96,7 @@ describe Diecut::Mill do
96
96
  end
97
97
 
98
98
  it "should produce a report" do
99
- expect(report).to match(/Template fields all have settings: WARN/)
99
+ expect(report).to match(/Template fields all have settings\s*WARN/)
100
100
  expect(report).to match(/Output field\s+Source file/)
101
101
  expect(report).to match(/thing\s+{{testing}}.txt/)
102
102
  expect(report).not_to match(/^\s*testing\b/)
@@ -112,7 +112,7 @@ describe Diecut::Mill do
112
112
  end
113
113
 
114
114
  it "should produce a report" do
115
- expect(report).to match(/Overridden context defaults: OK/)
115
+ expect(report).to match(/Overridden context defaults\s*OK/)
116
116
  end
117
117
  end
118
118
 
@@ -123,7 +123,7 @@ describe Diecut::Mill do
123
123
  end
124
124
 
125
125
  it "should produce a report" do
126
- expect(report).to match(/Overridden context defaults: FAIL/)
126
+ expect(report).to match(/Overridden context defaults\s*FAIL/)
127
127
  expect(report).to match(/Output field\s+Default value\s+Source plugin/)
128
128
  expect(report).to match(/thing\s+15\s+dummy/)
129
129
  expect(report).to match(/Total QA failing reports:/)
data/spec/spec_helper.rb CHANGED
@@ -9,4 +9,8 @@ RSpec.configure do |config|
9
9
  mock.verify_partial_doubles = true
10
10
  mock.verify_doubled_constant_names = true
11
11
  end
12
+
13
+ config.before(:suite) do
14
+ Diecut.issue_handler = Diecut::ErrorHandling::Silent.new
15
+ end
12
16
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diecut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Judson Lester
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-01 00:00:00.000000000 Z
11
+ date: 2016-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mustache
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">"
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">"
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
@@ -83,7 +83,8 @@ dependencies:
83
83
  description: ''
84
84
  email:
85
85
  - nyarly@gmail.com
86
- executables: []
86
+ executables:
87
+ - diecut
87
88
  extensions: []
88
89
  extra_rdoc_files: []
89
90
  files:
@@ -94,6 +95,7 @@ files:
94
95
  - lib/diecut/cli.rb
95
96
  - lib/diecut/configurable.rb
96
97
  - lib/diecut/context-handler.rb
98
+ - lib/diecut/error-report.rb
97
99
  - lib/diecut/errors.rb
98
100
  - lib/diecut/linter.rb
99
101
  - lib/diecut/mediator.rb
@@ -103,6 +105,15 @@ files:
103
105
  - lib/diecut/plugin-description/context-default.rb
104
106
  - lib/diecut/plugin-description/option.rb
105
107
  - lib/diecut/plugin-loader.rb
108
+ - lib/diecut/report-builder.rb
109
+ - lib/diecut/report-builders/exception-report.rb
110
+ - lib/diecut/report-builders/invalid-plugin.rb
111
+ - lib/diecut/report-builders/missing-context-field.rb
112
+ - lib/diecut/report-builders/option-collision.rb
113
+ - lib/diecut/report-builders/orphaned-field.rb
114
+ - lib/diecut/report-builders/overridden-context-defaults.rb
115
+ - lib/diecut/report-builders/template-list.rb
116
+ - lib/diecut/report-builders/unused-default.rb
106
117
  - lib/diecut/report.rb
107
118
  - lib/diecut/template-reducer.rb
108
119
  - lib/diecut/template-set.rb
@@ -129,7 +140,7 @@ rdoc_options:
129
140
  - "--main"
130
141
  - doc/README
131
142
  - "--title"
132
- - diecut-0.0.2 Documentation
143
+ - diecut-0.0.3 Documentation
133
144
  require_paths:
134
145
  - lib/
135
146
  required_ruby_version: !ruby/object:Gem::Requirement