bookwatch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/bookwatch.gemspec +40 -0
  3. data/install_bin/bookwatch +5 -0
  4. data/lib/bookwatch/cli.rb +109 -0
  5. data/lib/bookwatch/code_example_reader.rb +95 -0
  6. data/lib/bookwatch/colorizer.rb +16 -0
  7. data/lib/bookwatch/commands/bind.rb +119 -0
  8. data/lib/bookwatch/commands/collection.rb +181 -0
  9. data/lib/bookwatch/commands/components/bind/directory_preparer.rb +33 -0
  10. data/lib/bookwatch/commands/components/bind/layout_preparer.rb +27 -0
  11. data/lib/bookwatch/commands/components/command_options.rb +45 -0
  12. data/lib/bookwatch/commands/components/imprint/directory_preparer.rb +24 -0
  13. data/lib/bookwatch/commands/generate.rb +92 -0
  14. data/lib/bookwatch/commands/imprint.rb +55 -0
  15. data/lib/bookwatch/commands/punch.rb +33 -0
  16. data/lib/bookwatch/commands/update_local_doc_repos.rb +42 -0
  17. data/lib/bookwatch/commands/watch.rb +100 -0
  18. data/lib/bookwatch/config/checkers/archive_menu_checker.rb +29 -0
  19. data/lib/bookwatch/config/checkers/ditamap_presence_checker.rb +27 -0
  20. data/lib/bookwatch/config/checkers/duplicate_section_name_checker.rb +33 -0
  21. data/lib/bookwatch/config/checkers/products_checker.rb +34 -0
  22. data/lib/bookwatch/config/checkers/repository_name_presence_checker.rb +34 -0
  23. data/lib/bookwatch/config/checkers/required_keys_checker.rb +18 -0
  24. data/lib/bookwatch/config/checkers/section_presence_checker.rb +15 -0
  25. data/lib/bookwatch/config/configuration.rb +119 -0
  26. data/lib/bookwatch/config/configuration_decorator.rb +54 -0
  27. data/lib/bookwatch/config/dita_config_generator.rb +61 -0
  28. data/lib/bookwatch/config/fetcher.rb +64 -0
  29. data/lib/bookwatch/config/imprint/configuration.rb +24 -0
  30. data/lib/bookwatch/config/product_config.rb +34 -0
  31. data/lib/bookwatch/config/section_config.rb +85 -0
  32. data/lib/bookwatch/config/validator.rb +33 -0
  33. data/lib/bookwatch/config/yaml_loader.rb +34 -0
  34. data/lib/bookwatch/css_link_checker.rb +67 -0
  35. data/lib/bookwatch/directory_helpers.rb +15 -0
  36. data/lib/bookwatch/dita_command_creator.rb +95 -0
  37. data/lib/bookwatch/dita_html_for_middleman_formatter.rb +45 -0
  38. data/lib/bookwatch/errors/programmer_mistake.rb +5 -0
  39. data/lib/bookwatch/html_document_manipulator.rb +21 -0
  40. data/lib/bookwatch/ingest/cloner_factory.rb +26 -0
  41. data/lib/bookwatch/ingest/destination_directory.rb +21 -0
  42. data/lib/bookwatch/ingest/git_accessor.rb +102 -0
  43. data/lib/bookwatch/ingest/git_cloner.rb +36 -0
  44. data/lib/bookwatch/ingest/local_filesystem_cloner.rb +66 -0
  45. data/lib/bookwatch/ingest/missing_working_copy.rb +27 -0
  46. data/lib/bookwatch/ingest/repo_identifier.rb +45 -0
  47. data/lib/bookwatch/ingest/section_repository.rb +49 -0
  48. data/lib/bookwatch/ingest/update_failure.rb +15 -0
  49. data/lib/bookwatch/ingest/update_success.rb +12 -0
  50. data/lib/bookwatch/ingest/working_copy.rb +36 -0
  51. data/lib/bookwatch/local_filesystem_accessor.rb +122 -0
  52. data/lib/bookwatch/middleman_runner.rb +48 -0
  53. data/lib/bookwatch/postprocessing/link_checker.rb +125 -0
  54. data/lib/bookwatch/postprocessing/redirection.rb +38 -0
  55. data/lib/bookwatch/preprocessing/dita_html_preprocessor.rb +91 -0
  56. data/lib/bookwatch/preprocessing/dita_pdf_preprocessor.rb +48 -0
  57. data/lib/bookwatch/preprocessing/link_to_site_gen_dir.rb +39 -0
  58. data/lib/bookwatch/preprocessing/preprocessor.rb +26 -0
  59. data/lib/bookwatch/server_director.rb +30 -0
  60. data/lib/bookwatch/sheller.rb +52 -0
  61. data/lib/bookwatch/streams/colorized_stream.rb +25 -0
  62. data/lib/bookwatch/streams/filter_stream.rb +22 -0
  63. data/lib/bookwatch/subnav/navigation_entries_from_html_toc.rb +59 -0
  64. data/lib/bookwatch/subnav/navigation_entries_from_markdown_root.rb +116 -0
  65. data/lib/bookwatch/subnav/pdf_config_creator.rb +50 -0
  66. data/lib/bookwatch/subnav/subnav_generator.rb +28 -0
  67. data/lib/bookwatch/subnav/subnav_generator_factory.rb +29 -0
  68. data/lib/bookwatch/subnav/template_creator.rb +71 -0
  69. data/lib/bookwatch/terminal.rb +19 -0
  70. data/lib/bookwatch/values/output_locations.rb +91 -0
  71. data/lib/bookwatch/values/product_info.rb +11 -0
  72. data/lib/bookwatch/values/section.rb +58 -0
  73. data/lib/bookwatch/values/subnav_template.rb +4 -0
  74. data/lib/bookwatch/values/user_message.rb +15 -0
  75. data/master_middleman/archive_drop_down_menu.rb +50 -0
  76. data/master_middleman/bookwatch_helpers.rb +259 -0
  77. data/master_middleman/compass_runner.rb +0 -0
  78. data/master_middleman/config.rb +34 -0
  79. data/master_middleman/quicklinks_renderer.rb +80 -0
  80. data/master_middleman/source/javascripts/all.js +2 -0
  81. data/master_middleman/source/javascripts/book.js +1 -0
  82. data/master_middleman/source/javascripts/bookwatch.js +103 -0
  83. data/master_middleman/source/layouts/_additional-scripts.erb +0 -0
  84. data/master_middleman/source/layouts/_book-footer.erb +0 -0
  85. data/master_middleman/source/layouts/_book-search.erb +0 -0
  86. data/master_middleman/source/layouts/_book-title.erb +3 -0
  87. data/master_middleman/source/layouts/_header.erb +34 -0
  88. data/master_middleman/source/layouts/_local-header.erb +0 -0
  89. data/master_middleman/source/layouts/_page-footer.erb +1 -0
  90. data/master_middleman/source/layouts/_title.erb +5 -0
  91. data/master_middleman/source/layouts/layout.erb +69 -0
  92. data/master_middleman/source/stylesheets/all.css.scss +3 -0
  93. data/master_middleman/source/stylesheets/base.scss +380 -0
  94. data/master_middleman/source/stylesheets/book-styles.css.scss +0 -0
  95. data/master_middleman/source/stylesheets/layout-styles.scss +0 -0
  96. data/master_middleman/source/stylesheets/partials/_book-base-values.scss +0 -0
  97. data/master_middleman/source/stylesheets/partials/_book-vars.scss +0 -0
  98. data/master_middleman/source/stylesheets/partials/_default.scss +300 -0
  99. data/master_middleman/source/stylesheets/partials/_footer.scss +64 -0
  100. data/master_middleman/source/stylesheets/partials/_header.scss +419 -0
  101. data/master_middleman/source/stylesheets/partials/_layout-vars.scss +0 -0
  102. data/master_middleman/source/stylesheets/partials/_mixins.scss +53 -0
  103. data/master_middleman/source/stylesheets/partials/_reset.scss +233 -0
  104. data/master_middleman/source/stylesheets/partials/_search.scss +78 -0
  105. data/master_middleman/source/stylesheets/partials/_sidenav.scss +191 -0
  106. data/master_middleman/source/stylesheets/partials/_syntax-highlight.scss +64 -0
  107. data/master_middleman/source/stylesheets/partials/_vars.scss +64 -0
  108. data/master_middleman/source/stylesheets/print.css.scss +58 -0
  109. data/master_middleman/source/subnavs/_default.erb +0 -0
  110. data/master_middleman/source/subnavs/_nav-links.erb +10 -0
  111. data/master_middleman/source/subnavs/_subnav_template.erb +8 -0
  112. data/master_middleman/subdirectory_aware_assets.rb +47 -0
  113. data/template_app/Gemfile +10 -0
  114. data/template_app/Gemfile.lock +43 -0
  115. data/template_app/config.ru +9 -0
  116. data/template_app/lib/rack_static_if_exists.rb +19 -0
  117. data/template_app/lib/search/handler.rb +47 -0
  118. data/template_app/lib/search/hit.rb +21 -0
  119. data/template_app/lib/search/query.rb +74 -0
  120. data/template_app/lib/search/renderer.rb +29 -0
  121. data/template_app/lib/server.rb +52 -0
  122. data/template_app/mail_sender.rb +69 -0
  123. data/template_app/rack_app.rb +110 -0
  124. data/template_app/search-results.html.erb +75 -0
  125. data/template_app/search.yml +22 -0
  126. metadata +491 -0
@@ -0,0 +1,29 @@
1
+ module Bookwatch
2
+ module Config
3
+ module Checkers
4
+ class ArchiveMenuChecker
5
+ MissingArchiveMenuPartialError = Class.new(RuntimeError)
6
+ ArchiveMenuNotDefinedError = Class.new(RuntimeError)
7
+ EmptyArchiveItemsError = Class.new(RuntimeError)
8
+
9
+ def initialize(file_system_accessor)
10
+ @file_system_accessor = file_system_accessor
11
+ end
12
+
13
+ def check(config)
14
+ partial_location = './master_middleman/source/archive_menus/_default.erb'
15
+ if config.archive_menu && config.archive_menu.include?(nil)
16
+ EmptyArchiveItemsError.new 'Did you forget to add a value to the archive_menu?'
17
+ elsif config.archive_menu && !file_system_accessor.file_exist?(partial_location)
18
+ MissingArchiveMenuPartialError.new "You must provide a template partial named at #{partial_location}"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :file_system_accessor
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ module Bookwatch
2
+ module Config
3
+ module Checkers
4
+ class DitamapPresenceChecker
5
+ DitamapLocationError = Class.new(RuntimeError)
6
+
7
+ def check(config)
8
+ if any_sections_missing_ditamaps?(config.sections)
9
+ DitamapLocationError.new(
10
+ "You must have a 'ditamap_location' for each key in dita_sections."
11
+ )
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def any_sections_missing_ditamaps?(sections)
18
+ sections.any? do |s|
19
+ if s.preprocessor_config.has_key?('ditamap_location')
20
+ s.preprocessor_config['ditamap_location'].nil? || s.preprocessor_config['ditamap_location'].empty?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../../ingest/destination_directory'
2
+
3
+ module Bookwatch
4
+ module Config
5
+ module Checkers
6
+ class DuplicateSectionNameChecker
7
+ DuplicateSectionNameError = Class.new(RuntimeError)
8
+
9
+ def check(config)
10
+ if duplicate_section_names?(config)
11
+ DuplicateSectionNameError.new error_message
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def duplicate_section_names?(config)
18
+ directory_names = config.sections.map {|section|
19
+ Ingest::DestinationDirectory.new(section.repo_name,
20
+ section.desired_directory_name)
21
+ }
22
+ directory_names.length != directory_names.uniq.length
23
+ end
24
+
25
+ def error_message
26
+ <<-ERROR
27
+ Duplicate repository names are not allowed.
28
+ ERROR
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module Bookwatch
2
+ module Config
3
+ module Checkers
4
+ class ProductsChecker
5
+ MissingProductsKeyError = Class.new(RuntimeError)
6
+ MissingProductIdError = Class.new(RuntimeError)
7
+
8
+ def check(config)
9
+ @config = config
10
+
11
+ if section_product_ids.count > 0
12
+ if config.products.empty?
13
+ MissingProductsKeyError.new('You must specify at least one product under the products key in config.yml')
14
+ elsif missing_products.count != 0
15
+ MissingProductIdError.new("Your config.yml is missing required product id under the products key. Required product ids are #{missing_products.join(", ")}.")
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_reader :config
21
+
22
+ private
23
+
24
+ def missing_products
25
+ section_product_ids - config.products.map(&:id)
26
+ end
27
+
28
+ def section_product_ids
29
+ config.sections.map(&:product_id).compact.uniq
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module Bookwatch
2
+ module Config
3
+ module Checkers
4
+ class RepositoryNamePresenceChecker
5
+ MissingRepositoryNameError = Class.new(RuntimeError)
6
+
7
+ def check(config)
8
+ failures = config.sections.reject do |section|
9
+ section.repo_name
10
+ end
11
+
12
+ if failures.empty?
13
+ nil
14
+ else
15
+ MissingRepositoryNameError.new error_message
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def error_message
22
+ <<-ERROR
23
+ Cannot locate a specific section.
24
+ All sections must provide the section 'name' key under the 'repository' key:
25
+
26
+ sections:
27
+ - repository:
28
+ name: 'your-org/your-repo'
29
+ ERROR
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../configuration'
2
+
3
+ module Bookwatch
4
+ module Config
5
+ module Checkers
6
+ class RequiredKeysChecker
7
+ MissingRequiredKeyError = Class.new(RuntimeError)
8
+
9
+ def check(config)
10
+ missing_keys = Config::Configuration::CONFIG_REQUIRED_KEYS.reject { |key| config.has_option?(key) }
11
+ if missing_keys.any?
12
+ MissingRequiredKeyError.new("Your config.yml is missing required key(s). Required keys are #{missing_keys.join(", ")}.")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Bookwatch
2
+ module Config
3
+ module Checkers
4
+ class SectionPresenceChecker
5
+ NoSectionsError = Class.new(RuntimeError)
6
+
7
+ def check(config)
8
+ if config.sections.none?
9
+ NoSectionsError.new('No sections found in your config.yml. Add sections under the appropriate key(s) and try again.')
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,119 @@
1
+ require_relative '../../../lib/bookwatch/config/dita_config_generator'
2
+ require_relative '../ingest/destination_directory'
3
+ require_relative '../ingest/repo_identifier'
4
+ require_relative 'section_config'
5
+ require_relative 'product_config'
6
+
7
+ module Bookwatch
8
+ module Config
9
+ class Configuration
10
+ class << self
11
+ def parse(input_config)
12
+ section_configs = to_section_configs(combined_sections(input_config))
13
+ parse_sections(input_config, section_configs)
14
+ end
15
+
16
+ protected
17
+
18
+ def parse_sections(input_config, section_configs)
19
+ new(symbolize_keys(input_config).
20
+ merge(expand_repo_identifiers(input_config)).
21
+ merge(sections: section_configs))
22
+ end
23
+
24
+ def symbolize_keys(h)
25
+ h.reduce({}) {|acc, (k, v)| acc.merge(k.to_sym => v) }
26
+ end
27
+
28
+ def expand_repo_identifiers(input_config)
29
+ input_config.select {|k, _| k.match(/_repo$/)}.
30
+ reduce({}) {|h, (k, v)| h.merge(:"#{k}_url" => Ingest::RepoIdentifier.new(v))}
31
+ end
32
+
33
+ def to_section_configs sections
34
+ sections.map { |section| Config::SectionConfig.new(section) }
35
+ end
36
+
37
+ private
38
+
39
+ def combined_sections(input_config)
40
+ regular_sections(input_config) + dita_sections(input_config)
41
+ end
42
+
43
+ def regular_sections(input_config)
44
+ input_config['sections'] || []
45
+ end
46
+
47
+ def dita_sections(input_config)
48
+ (input_config['dita_sections'] || []).map { |dita_section|
49
+ DitaConfigGenerator.new(dita_section).to_hash
50
+ }
51
+ end
52
+ end
53
+
54
+ def initialize(config)
55
+ @config = config
56
+ @products = assemble_products || []
57
+ end
58
+
59
+ CONFIG_REQUIRED_KEYS = %w(book_repo public_host)
60
+ CONFIG_OPTIONAL_KEYS = %w(archive_menu book_repo_url cred_repo cred_repo_url repo_link_enabled repo_links feedback_enabled layout_repo layout_repo_ref layout_repo_url sections)
61
+
62
+ CONFIG_REQUIRED_KEYS.each do |method_name|
63
+ define_method(method_name) do
64
+ config.fetch(method_name.to_sym)
65
+ end
66
+ end
67
+
68
+ CONFIG_OPTIONAL_KEYS.each do |method_name|
69
+ define_method(method_name) do
70
+ config[method_name.to_sym]
71
+ end
72
+ end
73
+
74
+ def broken_link_exclusions
75
+ config.fetch(:broken_link_exclusions, /(?!.*)/)
76
+ end
77
+
78
+ def template_variables
79
+ config.fetch(:template_variables, {})
80
+ end
81
+
82
+ def merge(other_configuration)
83
+ Configuration.new(config.merge(other_configuration.instance_variable_get(:@config)))
84
+ end
85
+
86
+ def merge_sections(incoming_sections)
87
+ merge(Configuration.new(sections: sections + incoming_sections))
88
+ end
89
+
90
+ def has_option?(key)
91
+ !!config[key.to_sym]
92
+ end
93
+
94
+ def elastic_search?
95
+ config.fetch(:elastic_search, false)
96
+ end
97
+
98
+ def ==(o)
99
+ o.class == self.class && o.instance_variable_get(:@config) == @config
100
+ end
101
+
102
+ alias_method :eql?, :==
103
+
104
+ attr_reader :products
105
+
106
+ private
107
+
108
+ def assemble_products
109
+ if config[:products]
110
+ config[:products].map do |product|
111
+ Config::ProductConfig.new(product)
112
+ end
113
+ end
114
+ end
115
+
116
+ attr_reader :config
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,54 @@
1
+ require_relative 'configuration'
2
+
3
+ module Bookwatch
4
+ module Config
5
+ class ConfigurationDecorator
6
+ def initialize(loader: nil, config_filename: nil)
7
+ @loader = loader
8
+ @config_filename = config_filename
9
+ end
10
+
11
+ def generate(base_config, sections)
12
+ base_config.merge(
13
+ Configuration.new(
14
+ repo_links: repo_link_config(base_config, sections),
15
+ archive_menu: root_config(base_config).merge(section_config(sections)))
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :loader, :config_filename
22
+
23
+ def repo_link_config(base_config, sections)
24
+ if base_config.repo_link_enabled
25
+ sections.reduce({}) {|config, section|
26
+ config.merge(
27
+ section.destination_directory.to_s => {
28
+ 'repo' => section.repo_name,
29
+ 'ref' => section.source_ref,
30
+ 'at_path' => section.at_repo_path
31
+ }
32
+ )
33
+ }
34
+ end
35
+ end
36
+
37
+ def root_config(base_config)
38
+ { '.' => base_config.archive_menu }
39
+ end
40
+
41
+ def section_config(sections)
42
+ sections.reduce({}) {|config, section|
43
+ config_path = section.path_to_repo_dir.join(config_filename)
44
+ archive_config = loader.load_key(config_path, 'archive_menu')
45
+ if archive_config
46
+ config.merge(section.desired_directory_name => archive_config)
47
+ else
48
+ config
49
+ end
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ require_relative '../../../lib/bookwatch/ingest/destination_directory'
2
+
3
+ module Bookwatch
4
+ module Config
5
+ class DitaConfigGenerator
6
+
7
+ def initialize(section_hash)
8
+ @section_hash = section_hash
9
+ end
10
+
11
+ def subnav_template
12
+ dest_dir = Ingest::DestinationDirectory.new(section_hash.fetch('repository', {})['name'], section_hash['directory'])
13
+
14
+ "dita_subnav_#{dest_dir}"
15
+ end
16
+
17
+ def ditamap_location
18
+ section_hash['ditamap_location'] if section_hash['ditamap_location'] && !section_hash['ditamap_location'].empty?
19
+ end
20
+
21
+ def pdf_output_filename
22
+ if present?(section_hash['output_filename'])
23
+ filename = section_hash['output_filename']
24
+ elsif ditamap_location
25
+ filename = ditamap_location.gsub(/\.ditamap/, '')
26
+ else
27
+ return
28
+ end
29
+
30
+ filename + '.pdf'
31
+ end
32
+
33
+ def preprocessor_config
34
+ {
35
+ 'preprocessor_config' => {
36
+ 'ditamap_location' => ditamap_location,
37
+ 'ditaval_location' => section_hash['ditaval_location']
38
+ }
39
+ }
40
+ end
41
+
42
+ def to_hash
43
+ section_hash.tap do |hash|
44
+ hash.merge!(preprocessor_config)
45
+ .merge!('subnav_template' => subnav_template, 'output_filename' => pdf_output_filename)
46
+
47
+ hash.delete('ditaval_location')
48
+ hash.delete('ditamap_location')
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :section_hash
55
+
56
+ def present?(value)
57
+ value && !value.empty?
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'configuration'
2
+ require_relative 'yaml_loader'
3
+
4
+ module Bookwatch
5
+ module Config
6
+ class Fetcher
7
+ def initialize(configuration_validator, loader, config_class)
8
+ @loader = loader
9
+ @configuration_validator = configuration_validator
10
+ @config_class = config_class
11
+ end
12
+
13
+ def fetch_config
14
+ @base_config ||= read_config_file
15
+ @optional_configs ||= read_optional_configs
16
+
17
+ @config ||= validate(@base_config, @optional_configs)
18
+ end
19
+
20
+ def set_config_dir_path(config_dir_path)
21
+ @config_dir_path = File.expand_path(config_dir_path)
22
+ end
23
+
24
+ def set_config_file_path(config_file_path)
25
+ @config_file_path = File.expand_path(config_file_path)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader(:loader, :configuration_validator, :config, :config_file_path, :config_dir_path,
31
+ :config_class)
32
+
33
+ def read_config_file
34
+ loader.load(config_file_path)
35
+
36
+ rescue FileNotFoundError => e
37
+ raise "The configuration file specified does not exist. Please create a config #{e} file at #{config_file_path} and try again."
38
+ rescue InvalidSyntaxError => e
39
+ raise syntax_error(e)
40
+ end
41
+
42
+ def read_optional_configs
43
+ Dir["#{config_dir_path}/*.yml"].map do |config_file|
44
+ loader.load(File.expand_path(config_file)) || {}
45
+ end.reduce({}, :merge)
46
+ rescue InvalidSyntaxError => e
47
+ raise syntax_error(e)
48
+ end
49
+
50
+ def validate(base_hash, optional_hash)
51
+ raise 'Your config.yml appears to be empty. Please check and try again.' unless base_hash
52
+
53
+ config_class.parse(base_hash.merge(optional_hash)).tap do |config|
54
+ errors = configuration_validator.exceptions(config)
55
+ raise errors.first if errors.any?
56
+ end
57
+ end
58
+
59
+ def syntax_error(e)
60
+ "There is a syntax error in your config file: \n #{e}"
61
+ end
62
+ end
63
+ end
64
+ end