bauk-gen 0.0.2

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.
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bauk
4
+ module Gen
5
+ module ConfigUtils
6
+ # This error is to be raised every time there are errors with the config.
7
+ # These can usually be fixed by the user in a config file
8
+ class ConfigError < StandardError; end
9
+
10
+ # Method that can be used to validate individual config items.
11
+ # Takes an array of keys (or singular key) that specify the item to be checked
12
+ # (e.g. :a would check that {a: 123}, while [:a, :b] would check the following nested hash: {a: {b: 123}}
13
+ # The second argument is a hash of optional things to check.
14
+ # If this is not given, the check is merely to ensure that the value is set and not null.
15
+ # Optional items to check are:
16
+ # - allow_null : allows the value to be null or not exist
17
+ # - matches : specify a regex that the item needs to match
18
+ # - options : specify a list of valid options
19
+ # - message : specify an optional default error message to display
20
+ # - null_message : specify an optional error message to display if the value is nil
21
+ # - matches_message : specify an optional error message to display if the value is not in the valid list of options
22
+ # - options_message : specify an optional error message to display if the value does not match the given regex
23
+ # - default_value : Specify a default value in case of null
24
+ def validate_config_item(keys, map = {})
25
+ config_item = config
26
+ keys = [keys] unless keys.is_a? Array
27
+ parent_keys = keys.clone
28
+ key = parent_keys.pop
29
+ # Get value we are referring to
30
+ parent_keys.each do |parent_key|
31
+ config_item[parent_key] = {} unless config_item[parent_key].is_a?(Hash)
32
+ config_item = config_item[parent_key]
33
+ end
34
+ # Run checks
35
+ if config_item[key].nil?
36
+ if map[:default_value]
37
+ log.debug "Setting #{keys.join('->')} to default value: #{map[:default_value]}"
38
+ config_item[key] = map[:default_value]
39
+ else
40
+ map[:nil_message] ||= map[:message] || "Config item #{keys.join('->')} does not exist"
41
+ raise ConfigError, map[:nil_message] unless map[:allow_null]
42
+ end
43
+ elsif map[:matches] && config_item[key] !~ (map[:matches])
44
+ map[:matches_message] ||= map[:message] || "Config #{keys.join('->')}(#{config_item[key]}) does not match: '#{map[:matches]}'"
45
+ raise ConfigError, map[:matches_message]
46
+ elsif map[:options] && ! map[:options].include?(config_item[key])
47
+ map[:options_message] ||= map[:message] || "Config #{keys.join('->')}(#{config_item[key]}) needs to be one of: '#{map[:options].join(", ")}'"
48
+ raise ConfigError, map[:options_message]
49
+ end
50
+ end
51
+
52
+ # Method to check/validate the config being provided to the generator.
53
+ # It can, for example, be used to ensure mandatory config is present
54
+ # or that given config is in the correct format.
55
+ def validate_config; end
56
+
57
+ # Function copied from online thread
58
+ def underscore(camel_cased_word)
59
+ camel_cased_word.to_s.gsub(/::/, '/')
60
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
61
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
62
+ .tr('-', '_')
63
+ .downcase
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bauk/utils/log'
4
+ require 'deep_merge'
5
+
6
+ module Bauk
7
+ module Gen
8
+ module Configs
9
+ # Base class for all config generators.
10
+ # Each child class needs to overwrite the following method:
11
+ # - generate_config()
12
+ # -> This returns a hash of the config it obtains
13
+ class Base
14
+ include Bauk::Utils::Log
15
+
16
+ # Takes optional options:
17
+ # - keys: an array of where this config will be located in the final config hash
18
+ def initialize(map = {})
19
+ @keys = map[:keys] || []
20
+ end
21
+
22
+ # This method takes a config and adds the config that the config_generator
23
+ # will generate. It adds the generated config in a position on the hash
24
+ # dependant on which keys were passed to the config generator
25
+ def add_config!(config, keys = @keys)
26
+ log.debug 'adding config'
27
+ return config.deep_merge! generate_config if keys.empty?
28
+
29
+ key = keys.shift
30
+ config[key] = {} if (config[key] == true) || !(config[key])
31
+ # config[key] = add_config(config[key], keys)
32
+ add_config!(config[key], keys)
33
+ config
34
+ end
35
+
36
+ def symbolize(obj)
37
+ if obj.is_a? Hash
38
+ return obj.reduce({}) do |memo, (k, v)|
39
+ memo.tap { |m| m[k.to_sym] = symbolize(v) }
40
+ end
41
+ elsif obj.is_a? Array
42
+ return obj.each_with_object([]) do |v, memo|
43
+ memo << symbolize(v)
44
+ end
45
+ end
46
+
47
+ obj
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bauk
4
+ module Gen
5
+ module Configs
6
+ class Error < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bauk/utils/log'
4
+ require 'bauk/gen/configs/base'
5
+ require 'deep_merge'
6
+ require 'yaml'
7
+ require 'find'
8
+
9
+ module Bauk
10
+ module Gen
11
+ module Configs
12
+ class Files < Base
13
+ include Bauk::Utils::Log
14
+ def initialize(map = {})
15
+ super map
16
+ @files = map[:files] || default_files
17
+ @files << map[:extra_files] if map[:extra_files]
18
+ log.debug "Searching for the following files: #{@files.join ', '}"
19
+ sanitise_files
20
+ log.debug "Files used for config: #{YAML.dump @files}"
21
+ end
22
+
23
+ def default_files
24
+ [
25
+ '.bauk/generator/config.yaml',
26
+ '~/.bauk/generator/config.yaml'
27
+ ]
28
+ end
29
+
30
+ def sanitise_files
31
+ files_map = {}
32
+ @files = @files.select do |file|
33
+ path = file.is_a?(Hash) ? file[:path] : file
34
+ File.exist?(path) or Dir.exist?(path)
35
+ end.map do |file|
36
+ if file.is_a?(Hash)
37
+ file[:path] = File.expand_path file[:path]
38
+ file
39
+ else
40
+ {
41
+ path: File.expand_path(file)
42
+ }
43
+ end
44
+ end
45
+ @files.each do |file|
46
+ log.warn("Config file included twice: '#{file}'") if files_map[file[:path]]
47
+ files_map[file[:path]] = {}
48
+ end
49
+ end
50
+
51
+ # TODO: get working with custom base key
52
+ # Method that collects config from config files
53
+ def generate_config
54
+ conf = {}
55
+ log.warn 'No config files found' if @files.empty?
56
+ @files.each do |file_data|
57
+ file = file_data[:path]
58
+ if Dir.exist?(file)
59
+ log.debug "Adding config from dir: #{file}"
60
+ Find.find(file) do |path|
61
+ next if Dir.exist?(path)
62
+ base_keys = path.sub(/\.[a-z]*$/, '').sub(%r{^#{file}/*}, '').gsub('/', '.').split('.').map(&:to_sym)
63
+ log.debug("Adding nested config file '#{path}' as #{base_keys.join("->")}")
64
+ nested_config = conf
65
+ base_keys.each do |key|
66
+ nested_config[key] ||= {}
67
+ nested_config = nested_config[key]
68
+ end
69
+ nested_config.deep_merge!(symbolize(YAML.load_file(path)))
70
+ end
71
+ else
72
+ log.debug "Adding config from file: #{file}"
73
+ conf.deep_merge!(symbolize((YAML.load_file(file))))
74
+ end
75
+ end
76
+ conf
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'ostruct'
5
+
6
+ module Bauk
7
+ module Gen
8
+ module Contents
9
+ class BaseContent
10
+ include Bauk::Utils::Log
11
+ def initialize(opts)
12
+ @attributes = opts[:attributes]
13
+ @config = opts[:config]
14
+ end
15
+
16
+ def content
17
+ renderer = ERB.new(File.read(@file))
18
+ begin
19
+ renderer.result(OpenStruct.new(@config).instance_eval { binding })
20
+ rescue => e
21
+ log.error("ERROR IN FILE: #{@file}")
22
+ throw e
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'ostruct'
5
+ require 'json'
6
+ require 'bauk/gen/contents/base_content'
7
+
8
+ module Bauk
9
+ module Gen
10
+ module Contents
11
+ class ErbContent < BaseContent
12
+ SECTION_START_REGEX = /BAUK-GEN CUSTOM SECTION ([0-9A-Z_]+) START/
13
+ SECTION_END_REGEX = /BAUK-GEN CUSTOM SECTION ([0-9A-Z_]+) END/
14
+
15
+ def initialize(opts)
16
+ super(opts)
17
+ @file = opts[:file]
18
+ end
19
+
20
+ def content
21
+ renderer = ERB.new(File.read(@file))
22
+ erb_binding = OpenStruct.new(@config)
23
+ (((@config[:contents] ||= {})[:erb] ||= {})[:mixins] ||= []).each do |mixin|
24
+ erb_binding.extend(mixin)
25
+ end
26
+ begin
27
+ renderer.result(erb_binding.instance_eval { binding })
28
+ rescue => e
29
+ log.error("ERROR IN FILE: #{@file}")
30
+ throw e
31
+ end
32
+ end
33
+
34
+ def merge(current_content)
35
+ if @attributes[:merge] == true
36
+ merge_sections current_content, content
37
+ elsif @attributes[:merge] == "json"
38
+ merge_json current_content, content
39
+ else
40
+ raise "Invalid merge type provided: #{@attributes[:merge]} for template: #{@name}"
41
+ end
42
+ end
43
+
44
+ def merge_json(current_content, template_content)
45
+ current_json = JSON.parse(current_content)
46
+ template_json = JSON.parse(template_content)
47
+ if @attributes[:overwrite] == false
48
+ JSON.pretty_generate(template_json.deep_merge!(current_json))
49
+ else
50
+ JSON.pretty_generate(current_json.deep_merge!(template_json))
51
+ end
52
+ end
53
+
54
+ def merge_sections(current_content, template_content)
55
+ sections = {}
56
+ section = nil
57
+ section_no = nil
58
+ current_content.split("\n").each do |line|
59
+ if match = line.match(SECTION_START_REGEX)
60
+ section_no = match.captures[0]
61
+ raise "Section #{section_no} started inside previous section for file: #{@file}" if section
62
+ raise "Section #{section_no} has been defined more than once: #{@file}" if sections[section_no]
63
+ section = []
64
+ elsif match = line.match(SECTION_END_REGEX)
65
+ raise "Section #{match.captures[0]} ended before section started for file: #{@file}" unless section
66
+ raise "Secionn #{match.captures[0]} end block found inside section #{section_no} for file: #{@file}" unless section_no == match.captures[0]
67
+ sections[section_no] = section.join("\n")
68
+ section = nil
69
+ else
70
+ if section
71
+ section << line
72
+ end
73
+ end
74
+ end
75
+
76
+ new_content = []
77
+ section_no = nil
78
+ template_content.split("\n").each do |line|
79
+ if match = line.match(SECTION_START_REGEX)
80
+ raise "Section #{match.captures[0]} started inside previous section for template: #{@file}" if section_no
81
+ section_no = match.captures[0]
82
+ new_content << line
83
+ elsif match = line.match(SECTION_END_REGEX)
84
+ raise "Section #{match.captures[0]} ended before section started for template: #{@file}" unless section_no
85
+ raise "Secionn #{match.captures[0]} end block found inside section #{section_no} for template: #{@file}" unless section_no == match.captures[0]
86
+ if sections[section_no]
87
+ new_content.push(sections[section_no])
88
+ else
89
+ log.error "Section #{section_no} not found so replacing with template contents: #{@file}"
90
+ end
91
+ new_content << line
92
+ section_no = nil
93
+ else
94
+ unless section_no and sections[section_no]
95
+ new_content << line
96
+ end
97
+ end
98
+ end
99
+ new_content.join("\n")
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bauk/gen/contents/base_content'
4
+
5
+ module Bauk
6
+ module Gen
7
+ module Contents
8
+ class FileContent < BaseContent
9
+ attr_reader :file
10
+
11
+ def initialize(opts)
12
+ super(opts)
13
+ @file = File.new(opts[:file])
14
+ end
15
+
16
+ def content
17
+ @file.read
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bauk/gen/contents/base_content'
4
+
5
+ module Bauk
6
+ module Gen
7
+ module Contents
8
+ class StringContent < BaseContent
9
+ def initialize(opts)
10
+ super(opts)
11
+ @string = File.new(opts[:string])
12
+ end
13
+ def content
14
+ @string
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bauk/utils/log'
4
+ require 'bauk/gen/inputs/templates'
5
+ require 'bauk/gen/outputs/filesystem'
6
+ require 'bauk/gen/inputs/error'
7
+ require 'bauk/gen/configs/files'
8
+ require 'bauk/gen/config_utils'
9
+ require 'bauk/gen/init'
10
+ require 'deep_merge'
11
+
12
+ module Bauk
13
+ module Gen
14
+ # This class is the base generator that all others should extend from.
15
+ # It contains methods for listing inputs, outputs and trnasformations.
16
+ # The input generators cteate a map of items. These items are then handed to the output generators.
17
+ # In the base example, the only inout is from Templates and the output is the Filesystem output. So the templates are parsed to a hash, which is then passed to any transformers. After transformation, the items are passed to the Filesystem output.
18
+ class Generator
19
+ include Bauk::Utils::Log
20
+ include ConfigUtils
21
+ include Init
22
+ CONTENT_KEYS = %w[string file].freeze
23
+ # Map of items
24
+ # Example:
25
+ # {
26
+ # "path/to/file" => {
27
+ # content: {
28
+ # string: "...", # Could also be file: 'path/to/source',
29
+ # },
30
+ # name: "path/to/file",
31
+ # attributes: {
32
+ # merge: true,
33
+ # onetime: true
34
+ # },
35
+ # }
36
+ # }
37
+ @items = {}
38
+
39
+ def initialize(data)
40
+ @input_config = data[:config]
41
+ end
42
+
43
+ # This method lists modules that you want to include
44
+ def modules
45
+ []
46
+ end
47
+
48
+ # This function contains the default list of configs
49
+ def config_generators
50
+ [
51
+ Bauk::Gen::Configs::Files
52
+ ]
53
+ end
54
+
55
+ # This function contains the default list of inputs
56
+ def input_generators
57
+ [
58
+ Bauk::Gen::Inputs::Templates
59
+ ]
60
+ end
61
+
62
+ # This function contains the default list of outputs
63
+ def output_generators
64
+ [
65
+ Bauk::Gen::Outputs::Filesystem
66
+ ]
67
+ end
68
+
69
+ # This function contains the default transformations applied to each template
70
+ # TODO: It is still not implemented
71
+ def transformations
72
+ [
73
+ ]
74
+ end
75
+
76
+ def generate
77
+ log.warn "Generating #{name} generator"
78
+ validate_config
79
+ input_items
80
+ validate_items
81
+ output_items
82
+ log.warn "Finished generating #{@items.size} items"
83
+ end
84
+
85
+ # This function gets the items from the inputs
86
+ def input_items
87
+ log.info "Generating items for generator: #{name} (#{self.class.name})"
88
+ @items = {}
89
+ modules.each do |mod|
90
+ mod.input_items(@items)
91
+ end
92
+ input_generators.each do |i_gen|
93
+ i_gen.new(self, config).input_items(@items)
94
+ end
95
+ end
96
+
97
+ def validate_items
98
+ @items.each do |name, item|
99
+ item[:name] ||= name
100
+ item[:attributes] ||= {}
101
+ raise Inputs::Error, "No content found for item: #{name}" unless item[:content]
102
+
103
+ unless item[:content].respond_to? :content
104
+ raise Inputs::Error, "Invalid content found found. Found #{item[:content]}"
105
+ end
106
+ end
107
+ raise Inputs::Error, 'No items found to generate' if @items.empty?
108
+ end
109
+
110
+ # This function writes the items to outputs
111
+ def output_items
112
+ output_generators.each do |o_gen|
113
+ o_gen.new(self, config).output_items(@items)
114
+ end
115
+ end
116
+
117
+ # This method can be overridden to provide default values to config.
118
+ # These should be enough to get the generator/module working and are placed into the init config file
119
+ def default_config
120
+ {
121
+ config: {
122
+ name: "ExampleName",
123
+ description: "Example project description"
124
+ }
125
+ }
126
+ end
127
+
128
+ # Default config specific to this generator (injected at the generator level)
129
+ def default_generator_config
130
+ {}
131
+ end
132
+
133
+ # This function obtains the config for this generator
134
+ # Example config:
135
+ # c = {
136
+ # name: "Project Name",
137
+ # description: "Project Description",
138
+ # name => {
139
+ # custom_conf: 123
140
+ # },
141
+ # }
142
+ def config
143
+ if @config
144
+ return @config if name.empty?
145
+
146
+ return @config.deep_merge! @config[:generators][name]
147
+ end
148
+ @config = default_config.deep_merge!({generators:{name => default_generator_config }}).deep_merge!(@input_config)
149
+ config_generators.each do |c_gen|
150
+ c_gen.new.add_config! @config
151
+ end
152
+ unless @config
153
+ log.error 'No config found'
154
+ return {}
155
+ end
156
+ log.debug "Obtained config: #{@config}"
157
+ config
158
+ end
159
+
160
+ # Method to get generator name from class
161
+ def name
162
+ underscore(self.class.name.split('::').join('_').sub(/_*generator$/i, '')).to_sym
163
+ end
164
+ end
165
+ end
166
+ end