diecut 0.0.2 → 0.0.3

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