perron 1.0.0 → 1.2.0

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +11 -15
  3. data/app/controllers/perron/concierge_controller.rb +4 -1
  4. data/app/helpers/perron/markdown_helper.rb +3 -2
  5. data/app/helpers/perron/meta_tags_helper.rb +3 -9
  6. data/app/helpers/perron/paginate_helper.rb +40 -0
  7. data/app/views/perron/concierge/show.html.erb +1 -1
  8. data/lib/perron/collection.rb +2 -1
  9. data/lib/perron/configuration.rb +29 -4
  10. data/lib/perron/data_source.rb +6 -4
  11. data/lib/perron/deprecator.rb +1 -1
  12. data/lib/perron/engine.rb +11 -8
  13. data/lib/perron/feeds/split.rb +46 -0
  14. data/lib/perron/feeds.rb +17 -0
  15. data/lib/perron/html_processor/absolute_urls.rb +1 -1
  16. data/lib/perron/install/deploy.yml.erb +14 -0
  17. data/lib/perron/paginate.rb +58 -0
  18. data/lib/perron/relation.rb +27 -7
  19. data/lib/perron/resource/class_methods.rb +6 -0
  20. data/lib/perron/resource/configuration.rb +16 -3
  21. data/lib/perron/resource/metadata.rb +11 -4
  22. data/lib/perron/resource/publishable.rb +9 -1
  23. data/lib/perron/resource/separator.rb +11 -5
  24. data/lib/perron/resource/sourceable.rb +63 -9
  25. data/lib/perron/resource/updatable.rb +46 -0
  26. data/lib/perron/resource.rb +7 -0
  27. data/lib/perron/site/builder/feeds/atom.erb +1 -1
  28. data/lib/perron/site/builder/feeds/atom.rb +8 -2
  29. data/lib/perron/site/builder/feeds/json.rb +8 -2
  30. data/lib/perron/site/builder/feeds/rss.rb +8 -2
  31. data/lib/perron/site/builder/feeds.rb +32 -10
  32. data/lib/perron/site/builder/page.rb +19 -4
  33. data/lib/perron/site/builder/paths.rb +23 -1
  34. data/lib/perron/site/builder/sitemap.rb +23 -10
  35. data/lib/perron/site/builder.rb +24 -0
  36. data/lib/perron/site/validate.rb +18 -5
  37. data/lib/perron/tasks/deploy.rake +69 -0
  38. data/lib/perron/version.rb +1 -1
  39. data/lib/perron.rb +1 -0
  40. data/perron.gemspec +1 -1
  41. metadata +10 -4
@@ -54,7 +54,15 @@ module Perron
54
54
  return @date_from_filename if defined?(@date_from_filename)
55
55
 
56
56
  match = File.basename(file_path).match(DATE_REGEX)
57
- @date_from_filename = match ? Date.new(match[:year].to_i, match[:month].to_i, match[:day].to_i).in_time_zone : nil
57
+
58
+ @date_from_filename =
59
+ if match
60
+ begin
61
+ Date.new(match[:year].to_i, match[:month].to_i, match[:day].to_i).in_time_zone
62
+ rescue Date::Error
63
+ nil
64
+ end
65
+ end
58
66
  end
59
67
  end
60
68
  end
@@ -19,12 +19,18 @@ module Perron
19
19
 
20
20
  def parsed(content)
21
21
  if content =~ /\A---\s*(.*?)\s*---\s*(.*)/m
22
- @frontmatter = YAML.safe_load($1, permitted_classes: [Date, Time]) || {}
23
- @content = $2.strip
24
- else
25
- @frontmatter = {}
26
- @content = content
22
+ parsed_yaml = YAML.safe_load($1, permitted_classes: [Date, Time])
23
+
24
+ if parsed_yaml.nil? || parsed_yaml.is_a?(Hash)
25
+ @frontmatter = parsed_yaml || {}
26
+ @content = $2.strip
27
+
28
+ return
29
+ end
27
30
  end
31
+
32
+ @frontmatter = {}
33
+ @content = content
28
34
  end
29
35
  end
30
36
  end
@@ -5,6 +5,11 @@ module Perron
5
5
  module Sourceable
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ MODES = {
9
+ single: ->(dataset) { dataset.zip },
10
+ combinations: ->(dataset) { dataset.to_a.combination(2).to_a }
11
+ }
12
+
8
13
  class_methods do
9
14
  def sources(*arguments)
10
15
  @source_definitions = parsed(*arguments)
@@ -32,12 +37,14 @@ module Perron
32
37
  def generate_from_sources!
33
38
  return unless source_backed?
34
39
 
35
- combinations.each do |combo|
40
+ extension = source_definitions.values.first&.dig(:extension) || "erb"
41
+
42
+ derive.each do |combo|
36
43
  content = content_with combo
37
44
  filename = filename_with combo
38
45
 
39
46
  FileUtils.mkdir_p(output_dir)
40
- File.write(output_dir.join("#{filename}.erb"), content)
47
+ File.write(output_dir.join("#{filename}.#{extension}"), content)
41
48
  end
42
49
  end
43
50
 
@@ -48,7 +55,7 @@ module Perron
48
55
  def parsed(*arguments)
49
56
  return {} if arguments.empty?
50
57
 
51
- arguments.flat_map do |argument|
58
+ definitions = arguments.flat_map do |argument|
52
59
  case argument
53
60
  when Hash
54
61
  argument.to_a
@@ -58,11 +65,39 @@ module Perron
58
65
  [[argument, {primary_key: :id}]]
59
66
  end
60
67
  end.to_h
68
+
69
+ if definitions.values.any? { it[:mode] }
70
+ raise ArgumentError, "mode is only supported for single-source definitions" if definitions.size > 1
71
+ end
72
+
73
+ definitions
61
74
  end
62
75
 
63
- def combinations
76
+ def derive
64
77
  datasets = source_names.map { resolve it }
65
78
 
79
+ if single_source_with_mode?
80
+ derive_from_single_source(datasets.first)
81
+ else
82
+ derive_from_multiple_sources(datasets)
83
+ end
84
+ end
85
+
86
+ def derive_from_single_source(dataset)
87
+ source_name = source_names.first
88
+ mode = source_definitions[source_name][:mode] || :single
89
+ method = MODES[mode.to_sym] || raise(ArgumentError, "Unknown mode: #{mode}")
90
+
91
+ method.call(dataset).each do |combo|
92
+ primary_key = source_definitions[source_name][:primary_key] || :id
93
+
94
+ combo.each do |item|
95
+ raise Errors::DataParseError, "Primary key `#{primary_key}` is nil for row" if item.public_send(primary_key).nil?
96
+ end
97
+ end
98
+ end
99
+
100
+ def derive_from_multiple_sources(datasets)
66
101
  datasets.first.product(*datasets[1..]).each do |combo|
67
102
  combo.each_with_index do |item, index|
68
103
  name = source_names[index]
@@ -74,18 +109,37 @@ module Perron
74
109
  end
75
110
 
76
111
  def content_with(combo)
77
- data = source_names.each.with_index.to_h { |name, index| [name, combo[index]] }
112
+ data = if single_source_with_mode?
113
+ source_name = source_names.first
114
+ names = source_definitions[source_name][:as]&.map(&:to_sym) || (1..combo.size).map { :"#{source_name}_#{it}" }
115
+
116
+ combo.each_with_index.to_h { |item, index| [names[index], item] }
117
+ else
118
+ source_names.each_with_index.to_h { |name, index| [name, combo[index]] }
119
+ end
120
+
78
121
  source = Source.new(data)
79
122
 
80
123
  source_template(source)
81
124
  end
82
125
 
83
126
  def filename_with(combo)
84
- source_names.each_with_index.map do |name, index|
85
- primary_key = source_definitions[name][:primary_key]
127
+ if single_source_with_mode?
128
+ source_name = source_names.first
129
+ primary_key = source_definitions[source_name][:primary_key] || :id
130
+
131
+ combo.map { it.public_send(primary_key) }.join("-")
132
+ else
133
+ source_names.each_with_index.map do |name, index|
134
+ primary_key = source_definitions[name][:primary_key]
135
+
136
+ combo[index].public_send(primary_key)
137
+ end.join("-")
138
+ end
139
+ end
86
140
 
87
- combo[index].public_send(primary_key)
88
- end.join("-")
141
+ def single_source_with_mode?
142
+ source_names.one? && source_definitions[source_names.first][:mode]
89
143
  end
90
144
 
91
145
  def output_dir = Perron.configuration.input.join(model_name.collection)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perron
4
+ class Resource
5
+ module Updatable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ def metadata
10
+ @metadata ||= Proxy.new(super, self)
11
+ end
12
+ end
13
+
14
+ class Proxy
15
+ def initialize(metadata, resource)
16
+ @metadata = metadata
17
+ @resource = resource
18
+ end
19
+
20
+ def update(attributes)
21
+ attributes = attributes.deep_symbolize_keys
22
+
23
+ separator = Perron::Resource::Separator.new(@resource.raw_content)
24
+ frontmatter = separator.frontmatter.to_h.merge(attributes)
25
+ yaml = frontmatter.transform_keys(&:to_s).to_yaml.sub(/\A---\n/, "")
26
+ body = separator.content
27
+
28
+ File.write(@resource.file_path, "---\n#{yaml}---\n\n#{body}\n")
29
+
30
+ @resource.instance_variable_set(:@frontmatter, nil)
31
+ attributes.each { |key, value| @metadata[key] = value }
32
+
33
+ self
34
+ end
35
+
36
+ def respond_to_missing?(name, include_private = false)
37
+ @metadata.respond_to?(name, include_private) || super
38
+ end
39
+
40
+ def method_missing(name, *arguments, &block)
41
+ @metadata.public_send(name, *arguments, &block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -19,6 +19,7 @@ require "perron/resource/sourceable"
19
19
  require "perron/resource/sweeper"
20
20
  require "perron/resource/adjacency"
21
21
  require "perron/resource/table_of_content"
22
+ require "perron/resource/updatable"
22
23
 
23
24
  module Perron
24
25
  class Resource
@@ -96,6 +97,10 @@ module Perron
96
97
  slug == "/"
97
98
  end
98
99
 
100
+ def destroy
101
+ File.delete(@file_path) and self
102
+ end
103
+
99
104
  private
100
105
 
101
106
  ID_LENGTH = 8
@@ -117,6 +122,8 @@ module Perron
117
122
  end
118
123
 
119
124
  def erb_processing?
125
+ return false if metadata.erb == false
126
+
120
127
  @file_path.ends_with?(".erb") || metadata.erb == true
121
128
  end
122
129
  end
@@ -16,7 +16,7 @@
16
16
 
17
17
  <% resources.each do |resource| %>
18
18
  <entry>
19
- <id><%= url_for_resource.call(resource) || "#{@configuration.url}/posts/#{resource.id}" %></id>
19
+ <id><%= url_for_resource.call(resource) %></id>
20
20
  <title><%= resource.metadata.title %></title>
21
21
  <link href="<%= url_for_resource.call(resource) %>" rel="alternate" type="text/html"/>
22
22
  <published><%= resource.published_at&.iso8601 %></published>
@@ -11,8 +11,10 @@ module Perron
11
11
  include Feeds::Author
12
12
  include Feeds::Template
13
13
 
14
- def initialize(collection:)
14
+ def initialize(collection:, resources: nil, feed_config: nil)
15
15
  @collection = collection
16
+ @resources = resources
17
+ @feed_config = feed_config
16
18
  @configuration = Perron.configuration
17
19
  end
18
20
 
@@ -28,12 +30,16 @@ module Perron
28
30
  private
29
31
 
30
32
  def resources
31
- @resource ||= @collection.resources
33
+ (@resources || @collection.resources)
32
34
  .reject { it.metadata.feed == false }
33
35
  .sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
34
36
  .reverse
35
37
  .take(feed_configuration.max_items)
36
38
  end
39
+
40
+ def feed_configuration
41
+ @feed_config || super
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -11,8 +11,10 @@ module Perron
11
11
  include Feeds::Author
12
12
  include Feeds::Template
13
13
 
14
- def initialize(collection:)
14
+ def initialize(collection:, resources: nil, feed_config: nil)
15
15
  @collection = collection
16
+ @resources = resources
17
+ @feed_config = feed_config
16
18
  @configuration = Perron.configuration
17
19
  end
18
20
 
@@ -28,12 +30,16 @@ module Perron
28
30
  private
29
31
 
30
32
  def resources
31
- @resource ||= @collection.resources
33
+ (@resources || @collection.resources)
32
34
  .reject { it.metadata.feed == false }
33
35
  .sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
34
36
  .reverse
35
37
  .take(feed_configuration.max_items)
36
38
  end
39
+
40
+ def feed_configuration
41
+ @feed_config || super
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -11,8 +11,10 @@ module Perron
11
11
  include Feeds::Author
12
12
  include Feeds::Template
13
13
 
14
- def initialize(collection:)
14
+ def initialize(collection:, resources: nil, feed_config: nil)
15
15
  @collection = collection
16
+ @resources = resources
17
+ @feed_config = feed_config
16
18
  @configuration = Perron.configuration
17
19
  end
18
20
 
@@ -28,12 +30,16 @@ module Perron
28
30
  private
29
31
 
30
32
  def resources
31
- @resource ||= @collection.resources
33
+ (@resources || @collection.resources)
32
34
  .reject { it.metadata.feed == false }
33
35
  .sort_by { it.metadata.published_at || it.metadata.updated_at || Time.current }
34
36
  .reverse
35
37
  .take(feed_configuration.max_items)
36
38
  end
39
+
40
+ def feed_configuration
41
+ @feed_config || super
42
+ end
37
43
  end
38
44
  end
39
45
  end
@@ -4,10 +4,14 @@ require "perron/site/builder/feeds/atom"
4
4
  require "perron/site/builder/feeds/json"
5
5
  require "perron/site/builder/feeds/rss"
6
6
 
7
+ require "perron/feeds/split"
8
+
7
9
  module Perron
8
10
  module Site
9
11
  class Builder
10
12
  class Feeds
13
+ include Perron::Feeds::Split
14
+
11
15
  def initialize(output_path)
12
16
  @output_path = output_path
13
17
  end
@@ -18,21 +22,39 @@ module Perron
18
22
 
19
23
  config = collection.configuration.feeds
20
24
 
21
- if config.atom.enabled
22
- create_file at: config.atom.path, with: Atom.new(collection: collection).generate
23
- end
25
+ generate_feed(collection: collection, type_config: config.atom, generator_class: Atom) if config.atom.enabled
26
+ generate_feed(collection: collection, type_config: config.json, generator_class: Json) if config.json.enabled
27
+ generate_feed(collection: collection, type_config: config.rss, generator_class: Rss) if config.rss.enabled
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def generate_feed(collection:, type_config:, generator_class:)
34
+ generator = generator_class.new(collection: collection)
35
+ content = generator.generate
24
36
 
25
- if config.json.enabled
26
- create_file at: config.json.path, with: Json.new(collection: collection).generate
27
- end
37
+ create_file(at: type_config.path, with: content) if content.present?
28
38
 
29
- if config.rss.enabled
30
- create_file at: config.rss.path, with: Rss.new(collection: collection).generate
31
- end
39
+ return unless type_config[:split_by]
40
+
41
+ grouped_resources(collection.resources, type_config[:split_by][:extractor]).each do |value, group|
42
+ path = split_path_for(type_config, value)
43
+ config = split_config(type_config, value, path)
44
+ content = generator_class.new(collection: collection, resources: group, feed_config: config).generate
45
+
46
+ create_file(at: path, with: content) if content.present?
32
47
  end
33
48
  end
34
49
 
35
- private
50
+ def split_config(type_config, value, split_path)
51
+ ActiveSupport::OrderedOptions.new.tap do |config|
52
+ type_config.each { |key, value| config[key] = value unless key == :split_by }
53
+
54
+ config.path = split_path
55
+ config.title = "#{type_config.title.presence || Perron.configuration.site_name}: #{value.to_s.humanize}"
56
+ end
57
+ end
36
58
 
37
59
  def create_file(at:, with:)
38
60
  return if with.blank?
@@ -11,13 +11,15 @@ module Perron
11
11
  end
12
12
 
13
13
  def render
14
- action = route_info[:action]
14
+ info = route_info
15
+ return puts " ❌ ERROR: No route matches '#{@path}'" unless info
16
+
15
17
  request = ActionDispatch::Request.new(env)
16
18
  response = ActionDispatch::Response.new
17
19
 
18
- request.path_parameters = route_info
20
+ request.path_parameters = info
19
21
 
20
- controller.dispatch(action, request, response)
22
+ controller.dispatch(info[:action], request, response)
21
23
 
22
24
  return puts " ❌ ERROR: Request failed for '#{@path}' (Status: #{response.status})" unless response.successful?
23
25
 
@@ -41,7 +43,20 @@ module Perron
41
43
  end
42
44
 
43
45
  def route_info
44
- @route_info ||= Rails.application.routes.recognize_path(@path)
46
+ @route_info ||= recognize_path_with_pagination_fallback(@path)
47
+ end
48
+
49
+ def recognize_path_with_pagination_fallback(path)
50
+ Rails.application.routes.recognize_path(path)
51
+ rescue ActionController::RoutingError
52
+ return nil unless (match = path.match(/\/(.+)\/page\/(\d+)\//))
53
+
54
+ base_path = "/#{match[1]}"
55
+ page_number = match[2].to_i
56
+
57
+ Rails.application.routes.recognize_path(base_path).tap do |route_info|
58
+ route_info[:page] = page_number
59
+ end
45
60
  end
46
61
 
47
62
  def env = Rack::MockRequest.env_for(@path, "HTTP_HOST" => Perron.configuration.default_url_options[:host])
@@ -28,12 +28,30 @@ module Perron
28
28
  raise "Route `#{route.name}` (#{route.path.spec}) is an index route but requires parameters #{required_params}. Perron doesn't know how to generate these parameters."
29
29
  end
30
30
 
31
- [routes.public_send("#{route.name}_path")]
31
+ base_path = routes.public_send("#{route.name}_path")
32
+ collection = collection_for(route)
33
+ return [base_path] unless collection
34
+
35
+ pagination = collection.configuration.pagination
36
+ return [base_path] unless pagination.per_page
37
+
38
+ total = collection.all.count
39
+ total_pages = (total.to_f / pagination.per_page).ceil
40
+ return [base_path] if total_pages <= 1
41
+
42
+ [base_path] + (2..total_pages).map do |page_number|
43
+ build_paginated_path(route, page_number, pagination.path_template)
44
+ end
32
45
  when "show" then show_paths_for(route)
33
46
  else []
34
47
  end
35
48
  end
36
49
 
50
+ def build_paginated_path(route, page_number, path_template)
51
+ routes.public_send("#{route.name}_path")
52
+ .sub(/\/$/, "") + path_template.sub(":page", page_number.to_s)
53
+ end
54
+
37
55
  def show_paths_for(route)
38
56
  resources_for(route).reject(&:root?).map do |resource|
39
57
  routes.public_send("#{route.name}_path", resource)
@@ -73,6 +91,10 @@ module Perron
73
91
  def sitemap_change_frequency = nil
74
92
 
75
93
  def updated_at = nil
94
+
95
+ def canonical_url = nil
96
+
97
+ def explicit_canonical_url = nil
76
98
  end
77
99
  end
78
100
  end
@@ -52,9 +52,8 @@ module Perron
52
52
  routes.with_options(Perron.configuration.default_url_options) do |url|
53
53
  with.url do
54
54
  with.loc url.public_send(route_name.to_s.gsub("_path", "_url"))
55
- with.priority Perron.configuration.sitemap.priority
56
- with.changefreq Perron.configuration.sitemap.change_frequency
57
- with.lastmod Time.current.iso8601
55
+ with.priority Perron.configuration.sitemap.priority if Perron.configuration.sitemap.emit_priority
56
+ with.changefreq Perron.configuration.sitemap.change_frequency if Perron.configuration.sitemap.emit_changefreq
58
57
  end
59
58
  end
60
59
  end
@@ -76,8 +75,8 @@ module Perron
76
75
 
77
76
  with.url do
78
77
  with.loc loc
79
- with.priority Perron.configuration.sitemap.priority
80
- with.changefreq Perron.configuration.sitemap.change_frequency
78
+ with.priority Perron.configuration.sitemap.priority if Perron.configuration.sitemap.emit_priority
79
+ with.changefreq Perron.configuration.sitemap.change_frequency if Perron.configuration.sitemap.emit_changefreq
81
80
  with.lastmod lastmod if lastmod
82
81
  end
83
82
  end
@@ -95,14 +94,15 @@ module Perron
95
94
 
96
95
  loc = resource.root? ? routes.root_url(url_options) : routes.public_send("#{route.name}_url", resource, **url_options)
97
96
 
97
+ return if canonical_mismatch?(resource.metadata.explicit_canonical_url, loc)
98
98
  return if added_urls.include?(loc)
99
99
  added_urls << loc
100
100
 
101
101
  routes.with_options(Perron.configuration.default_url_options) do |url|
102
102
  with.url do
103
103
  with.loc loc
104
- with.priority priority
105
- with.changefreq change_frequency
104
+ with.priority priority if Perron.configuration.sitemap.emit_priority
105
+ with.changefreq change_frequency if Perron.configuration.sitemap.emit_changefreq
106
106
  with.lastmod resource.metadata.updated_at&.iso8601 if resource.metadata.updated_at.present?
107
107
  end
108
108
  end
@@ -111,10 +111,23 @@ module Perron
111
111
  def last_modified_for(collection)
112
112
  return unless collection
113
113
 
114
- resources = collection.send(:load_resources).select(&:buildable?)
115
- dates = resources.filter_map { it.metadata.updated_at || it.metadata.publication_date }
114
+ modification_dates = collection.send(:load_resources).select(&:buildable?).filter_map { it.metadata.updated_at }
116
115
 
117
- dates.max&.iso8601
116
+ modification_dates.max&.iso8601
117
+ end
118
+
119
+ def canonical_mismatch?(canonical, location)
120
+ return false unless canonical.is_a?(String) && canonical.present?
121
+
122
+ canonical_uri = URI.parse(canonical)
123
+ canonical_uri = URI.join(location, canonical) unless canonical_uri.host
124
+ page_uri = URI.parse(location)
125
+
126
+ normalize = ->(uri) { [uri.host, uri.path].map(&:to_s).map(&:downcase).join("|") }
127
+
128
+ normalize.call(canonical_uri) != normalize.call(page_uri)
129
+ rescue URI::InvalidURIError
130
+ false
118
131
  end
119
132
 
120
133
  def routes = Rails.application.routes.url_helpers
@@ -16,6 +16,8 @@ module Perron
16
16
  end
17
17
 
18
18
  def build
19
+ run_hook(:before_build)
20
+
19
21
  if Perron.configuration.mode.standalone?
20
22
  puts "🧹 Cleaning previous build…"
21
23
 
@@ -35,6 +37,8 @@ module Perron
35
37
  output_preview_urls
36
38
 
37
39
  puts "\n✅ Build complete"
40
+ ensure
41
+ run_hook(:after_build)
38
42
  end
39
43
 
40
44
  private
@@ -58,6 +62,26 @@ module Perron
58
62
  end
59
63
  end
60
64
  end
65
+
66
+ def run_hook(name)
67
+ return unless (hook = Perron.configuration.public_send(name))
68
+
69
+ context = Context.new(
70
+ output_path: @output_path.to_s,
71
+ mode: Perron.configuration.mode
72
+ )
73
+
74
+ hook.call(context)
75
+ end
76
+
77
+ class Context
78
+ attr_reader :output_path, :mode
79
+
80
+ def initialize(output_path:, mode:)
81
+ @output_path = output_path
82
+ @mode = mode
83
+ end
84
+ end
61
85
  end
62
86
  end
63
87
  end
@@ -36,7 +36,11 @@ module Perron
36
36
  def validate_collection(collection)
37
37
  collection.resources.each do |resource|
38
38
  resource.validate ? success : failed(resource)
39
+ rescue Psych::SyntaxError => error
40
+ render_yaml error, resource.file_path
39
41
  end
42
+ rescue Psych::SyntaxError => error
43
+ render_yaml error, "unknown"
40
44
  end
41
45
 
42
46
  def success = print "#{GREEN}.#{RESET}"
@@ -44,13 +48,22 @@ module Perron
44
48
  def failed(resource)
45
49
  print "#{RED}F#{RESET}"
46
50
 
47
- errors = []
51
+ @failures << Failure.new(
52
+ identifier: resource.file_path,
53
+ errors: resource.errors.respond_to?(:full_messages) ? resource.errors.full_messages : []
54
+ )
55
+ end
48
56
 
49
- if resource.respond_to?(:errors) && resource.errors.respond_to?(:full_messages) && resource.errors.any?
50
- errors = resource.errors.full_messages
51
- end
57
+ def render_yaml(error, identifier)
58
+ print "#{RED}F#{RESET}"
59
+
60
+ line_info = error.line ? " at line #{error.line}" : ""
61
+ column_info = error.column ? ", column #{error.column}" : ""
52
62
 
53
- @failures << Failure.new(identifier: resource.file_path, errors: errors)
63
+ @failures << Failure.new(
64
+ identifier: identifier,
65
+ errors: ["Invalid YAML#{line_info}#{column_info}: #{error.problem}"]
66
+ )
54
67
  end
55
68
 
56
69
  def failures_report