bauk-gen 0.0.2

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