crossplane 0.1.5

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,203 @@
1
+ require 'crossplane/globals'
2
+ require 'json'
3
+ require 'pathname'
4
+ require 'pp'
5
+
6
+ #require_relative 'globals.rb'
7
+
8
+ module CrossPlane
9
+ class Builder
10
+ DELIMITERS = ['{', '}', ';']
11
+ EXTERNAL_BUILDERS = {}
12
+ NEWLINE = "\n"
13
+ TAB = "\t"
14
+
15
+ attr_accessor :header
16
+ attr_accessor :indent
17
+ attr_accessor :padding
18
+ attr_accessor :payload
19
+ attr_accessor :state
20
+ attr_accessor :tabs
21
+
22
+ def initialize(*args)
23
+ args = args[0] || {}
24
+
25
+ required = ['payload']
26
+ conflicts = []
27
+ requires = {}
28
+ valid = {
29
+ 'params' => [
30
+ 'header',
31
+ 'indent',
32
+ 'payload',
33
+ 'tabs',
34
+ ]
35
+ }
36
+
37
+ content = CrossPlane.utils.validate_constructor(client: self, args: args, required: required, conflicts: conflicts, requires: requires, valid: valid)
38
+ self.header = (content[:header] && content[:header] == true) ? true : false
39
+ self.indent = content[:indent] ? content[:indent] : 4
40
+ self.payload = content[:payload] ? content[:payload] : nil
41
+ self.tabs = (content[:tabs] && content[:tabs] == true) ? true : false
42
+ end
43
+
44
+ def build(*args)
45
+ self.padding = self.tabs ? TAB : ' ' * self.indent
46
+ self.state = {
47
+ 'prev_obj' => nil,
48
+ 'depth' => -1,
49
+ }
50
+
51
+ if self.header
52
+ lines = [
53
+ "# This config was built from JSON using NGINX crossplane.\n",
54
+ "# If you encounter any bugs please report them here:\n",
55
+ "# https://github.com/gdanko/crossplane/issues\n",
56
+ "\n"
57
+ ]
58
+ else
59
+ lines = []
60
+ end
61
+
62
+ lines += _build_lines(payload)
63
+ puts lines.join('')
64
+ exit
65
+ return lines.join('')
66
+ end
67
+
68
+ private
69
+ def _put_line(line, obj)
70
+ margin = self.padding * self.state['depth']
71
+
72
+ # don't need put \n on first line and after comment
73
+ if self.state['prev_obj'].nil?
74
+ return margin + line
75
+ end
76
+
77
+ # trailing comments have to be without \n
78
+ if obj['directive'] == '#' and obj['line'] == self.state['prev_obj']['line']
79
+ return ' ' + line
80
+ end
81
+
82
+ return NEWLINE + margin + line
83
+ end
84
+
85
+ def _build_lines(objs)
86
+ lines = Enumerator.new do |y|
87
+ self.state['depth'] = self.state['depth'] + 1
88
+
89
+ objs.each do |obj|
90
+ directive = obj['directive']
91
+ if EXTERNAL_BUILDERS[directive]
92
+ #built = external_builder(obj, padding, state)
93
+ #y.yield(_put_line(built_obj))
94
+ #next
95
+ end
96
+
97
+ if directive == '#'
98
+ y.yield(_put_line(
99
+ '#' + obj['comment'],
100
+ obj
101
+ ))
102
+ next
103
+ end
104
+
105
+ args = obj['args'].map{|arg| _enquote(arg)}
106
+
107
+ if directive == 'if'
108
+ line = format('if (%s)', args.join(' '))
109
+ elsif args
110
+ line = format('%s %s', directive, args.join(' '))
111
+ else
112
+ line = directive
113
+ end
114
+
115
+ if not obj.key?('block')
116
+ y.yield(_put_line(line + ';', obj))
117
+ else
118
+ y.yield(_put_line(line + ' {', obj))
119
+
120
+ # set prev_obj to propper indentation in block
121
+ self.state['prev_obj'] = obj
122
+ _build_lines(obj['block']).each do |line|
123
+ y.yield(line)
124
+ end
125
+ y.yield(_put_line('}', obj))
126
+ end
127
+ self.state['prev_obj'] = obj
128
+ end
129
+ self.state['depth'] = self.state['depth'] - 1
130
+ end
131
+ lines.to_a
132
+ end
133
+
134
+ def _escape(string)
135
+ chars = Enumerator.new do |y|
136
+ prev, char = '', ''
137
+ string.split('').each do |char|
138
+ if prev == '\\' or prev + char == '${'
139
+ prev += char
140
+ y.yield char
141
+ next
142
+ end
143
+
144
+ if prev == '$'
145
+ y.yield prev
146
+ end
147
+
148
+ if not ['\\', '$'].include?(char)
149
+ y.yield char
150
+ end
151
+ prev = char
152
+ end
153
+
154
+ if ['\\', '$'].include?(char)
155
+ y.yield char
156
+ end
157
+ end
158
+ chars
159
+ end
160
+
161
+ def _needs_quotes(string)
162
+ if string == ''
163
+ return true
164
+ elsif DELIMITERS.include?(string)
165
+ return false
166
+ end
167
+
168
+ # lexer should throw an error when variable expansion syntax
169
+ # is messed up, but just wrap it in quotes for now I guess
170
+ chars = _escape(string)
171
+
172
+ begin
173
+ while char = chars.next
174
+
175
+
176
+ # arguments can't start with variable expansion syntax
177
+ if CrossPlane.utils.isspace(char) or ['{', ';', '"', "'", '${'].include?(char)
178
+ return true
179
+ end
180
+
181
+ expanding = false
182
+ #chars.each do |char|
183
+ # if CrossPlane.utils.isspace(char) or ['{', ';', '"', "'"].include(char)
184
+ # return true
185
+ # elsif char ==
186
+ # char in ('\\', '$') or expanding
187
+ return expanding
188
+ end
189
+ rescue StopIteration
190
+ end
191
+ end
192
+
193
+ def _enquote(arg)
194
+ if _needs_quotes(arg)
195
+ #arg = repr(codecs.decode(arg, 'raw_unicode_escape'))
196
+ arg = arg.gsub('\\\\', '\\')
197
+ end
198
+ return arg
199
+ end
200
+
201
+
202
+ end
203
+ end
@@ -0,0 +1,139 @@
1
+ require 'crossplane/builder'
2
+ require 'crossplane/config'
3
+ require 'crossplane/parser'
4
+ require 'json'
5
+ require 'logger'
6
+ require 'pp'
7
+ require 'thor'
8
+ require 'yaml'
9
+
10
+ #require_relative 'builder.rb'
11
+ #require_relative 'config.rb'
12
+ #require_relative 'parser.rb'
13
+
14
+ $script = File.basename($0)
15
+ $config = CrossPlane::Config.new()
16
+
17
+ trap('SIGINT') {
18
+ puts("\nControl-C received.")
19
+ exit(0)
20
+ }
21
+
22
+ def configure_options(thor, opt_type, opts)
23
+ opts = opts.sort_by { |k| k[:name].to_s }
24
+ opts.each do |opt|
25
+ required = opt.key?(:required) ? opt[:required] : false
26
+ aliases = opt.key?(:aliases) ? opt[:aliases] : []
27
+ if opt_type == "class"
28
+ thor.class_option(opt[:name], :banner => opt[:banner], :desc => opt[:desc], :aliases => aliases, :required => required, :type => opt[:type])
29
+ elsif opt_type == "method"
30
+ thor.method_option(opt[:name], :banner => opt[:banner], :desc => opt[:desc], :aliases => aliases, :required => required, :type => opt[:type])
31
+ end
32
+ end
33
+ end
34
+
35
+ class CLI < Thor
36
+ desc 'parse <filename>', 'parses an nginx config file and returns a json payload'
37
+ configure_options(self, 'method', $config.parse_options)
38
+ def parse(filename)
39
+ payload = CrossPlane::Parser.new(
40
+ filename: filename,
41
+ combine: options['combine'] || false,
42
+ strict: options['strict'] || false,
43
+ catch_errors: options['no_catch'] ? false : true,
44
+ comments: options['include_comments'] || false,
45
+ ignore: options['ignore'] ? options['ignore'].split(/\s*,\s*/) : [],
46
+ single: options['single'] || false,
47
+ ).parse()
48
+
49
+ if options['out']
50
+ File.open(options['out'], 'w') do |f|
51
+ f.write(JSON.pretty_generate(payload))
52
+ end
53
+ else
54
+ puts options['pretty'] ? JSON.pretty_generate(payload) : payload.to_json
55
+ end
56
+ exit 0
57
+ end
58
+
59
+ desc 'build <filename>', 'builds an nginx config from a json payload'
60
+ configure_options(self, 'method', $config.build_options)
61
+ def build(filename)
62
+ dirname = Dir.pwd unless dirname
63
+
64
+ # read the json payload from the specified file
65
+ payload = JSON.parse(File.read(filename))
66
+ builder = CrossPlane::Builder.new(
67
+ payload: payload['config'][0]['parsed']
68
+ )
69
+
70
+ if not options['force'] and not options['stdout']
71
+ existing = []
72
+ payload['config'].each do |config|
73
+ path = config['file']
74
+ p = Pathname.new(path)
75
+ path = p.absolute? ? path: File.join(dirname, path)
76
+ if File.exist?(path)
77
+ existing.push(path)
78
+ end
79
+ end
80
+
81
+
82
+ # ask the user if it's okay to overwrite existing files
83
+ if existing.length > 0
84
+ puts(format('building %s would overwrite these files:', filename))
85
+ puts existing.join("\n")
86
+ # if not _prompt_yes():
87
+ # print('not overwritten')
88
+ # return
89
+ end
90
+ end
91
+
92
+ # if stdout is set then just print each file after another like nginx -T
93
+ #if options['stdout']
94
+ payload['config'].each do |config|
95
+ path = config['file']
96
+ p = Pathname.new(path)
97
+ path = p.absolute? ? path: File.join(dirname, path)
98
+ parsed = config['parsed']
99
+ output = builder.build(
100
+ parsed,
101
+ indent: options['indent'] || 4, # fix default option in config.rb
102
+ tabs: options['tabs'],
103
+ header: options['header']
104
+ )
105
+ output = output.rstrip + "\n"
106
+ end
107
+ return
108
+ #end
109
+ end
110
+
111
+ desc 'lex <filename>', 'lexes tokens from an nginx config file'
112
+ configure_options(self, 'method', $config.lex_options)
113
+ def lex(filename)
114
+ payload = CrossPlane::Lexer.new(
115
+ filename: filename,
116
+ ).lex()
117
+ lex = (not options['line_numbers'].nil? and options['line_numbers'] == true) ? payload : payload.map{|e| e[0]}
118
+
119
+ if options['out']
120
+ File.open(options['out'], 'w') do |f|
121
+ f.write(JSON.pretty_generate(lex))
122
+ end
123
+ else
124
+ puts options['pretty'] ? JSON.pretty_generate(lex) : lex.to_json
125
+ end
126
+ end
127
+
128
+ desc 'minify', 'removes all whitespace from an nginx config'
129
+ def minify(filename)
130
+ puts 'minifiy'
131
+ exit
132
+ end
133
+
134
+ #desc 'format', 'formats an nginx config file'
135
+ #def format(filename)
136
+ # puts 'format'
137
+ # exit
138
+ #end
139
+ end
@@ -0,0 +1,56 @@
1
+ module CrossPlane
2
+ class Config
3
+ attr_accessor :opts
4
+ attr_accessor :common_opts
5
+ attr_accessor :parse_opts
6
+ attr_accessor :build_opts
7
+ attr_accessor :lex_opts
8
+ attr_accessor :minify_opts
9
+ attr_accessor :format_opts
10
+
11
+ def initialize(*args)
12
+ self.common_opts = {
13
+
14
+ }
15
+
16
+ self.parse_opts = {
17
+ 'out' => {:name => :out, :aliases => ['-o'], :banner => '<string>', :desc => 'write output to a file', :type => :string, :required => false},
18
+ 'pretty' => {:name => :pretty, :desc => 'pretty print the json output', :type => :boolean, :required => false, :default => :false},
19
+ 'ignore' => {:name => :ignore, :banner => '<str>', :desc => 'ignore directives (comma-separated)', :type => :string, :required => false, :default => []},
20
+ 'no-catch' => {:name => :no_catch, :desc => 'only collect first error in file', :type => :boolean, :required => false, :default => :false},
21
+ #'tb-onerror' => {:name => :tb_onerror, :desc => 'include tracebacks in config errors', :type => :boolean, :required => false},
22
+ #'combine' => {:name => :combine, :desc => 'use includes to create one single file', :type => :boolean, :required => false},
23
+ #'single' => {:name => :single, :desc => 'do not include other config files', :type => :boolean, :required => false},
24
+ 'include-comments' => {:name => :include_comments, :desc => 'include comments in json', :type => :boolean, :required => false, :default => :false},
25
+ 'strict' => {:name => :strict, :desc => 'raise errors for unknown directives', :type => :boolean, :required => false, :default => :false},
26
+ }
27
+
28
+ self.build_opts = {
29
+ 'dir' => {:name => :dir, :banner => '<string>', :desc => 'the base directory to build in', :type => :string, :required => false},
30
+ 'force' => {:name => :force, :desc => 'overwrite existing files', :type => :boolean, :required => false},
31
+ 'indent' => {:name => :indent, :banner => '<string>', :desc => 'number of spaces to indent output', :type => :numeric, :required => false, :default => 4},
32
+ 'tabs' => {:name => :tabs, :desc => 'indent with tabs instead of spaces', :type => :boolean, :required => false},
33
+ #'no-headers' => {:name => :no_header2, :desc => 'do not write header to configsd', :type => :boolean, :required => false},
34
+ 'stdout' => {:name => :stdout, :desc => 'write configs to stdout instead', :type => :boolean, :required => false},
35
+ }
36
+
37
+ self.lex_opts = {
38
+ 'out' => {:name => :out, :aliases => ['-o'], :banner => '<string>', :desc => 'write output to a file', :type => :string, :required => false},
39
+ 'indent' => {:name => :indent, :aliases => ['-i'], :banner => '<int>', :desc => 'number of spaces to indent output', :type => :numeric, :required => false, :default => 4},
40
+ 'numbers' => {:name => :line_numbers, :aliases => ['-n'], :desc => 'include line numbers in json payload', :type => :boolean, :required => false, :default => true},
41
+ }
42
+ end
43
+
44
+ def parse_options()
45
+ return self.common_opts.merge(self.parse_opts).map { |_k, v| v }
46
+ end
47
+
48
+ def build_options()
49
+ return self.common_opts.merge(self.build_opts).map { |_k, v| v }
50
+ end
51
+
52
+ def lex_options()
53
+ return self.common_opts.merge(self.lex_opts).map { |_k, v| v }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,64 @@
1
+ module CrossPlane
2
+ class ConstructorError < StandardError
3
+ def initialize(errors: nil)
4
+ @errors = errors
5
+ @error = @errors.join('; ')
6
+ super(@error)
7
+ end
8
+ end
9
+
10
+ class NgxParserBaseException < StandardError
11
+ attr_reader :filename, :lineno, :strerror
12
+ def initialize(filename, lineno, strerror)
13
+ @filename = filename
14
+ @lineno = lineno
15
+ @strerror = strerror
16
+ if @lineno.nil?
17
+ @error = format('%s in %s', @strerror, @filename)
18
+ else
19
+ @error = format('%s in %s:%s', @strerror, @filename, @lineno)
20
+ end
21
+ super(@error)
22
+ end
23
+ end
24
+
25
+ class NgxParserDirectiveError < StandardError
26
+ def initialize(reason, filename, lineno)
27
+ @reason = reason
28
+ @filename = filename
29
+ @lineno = lineno
30
+ @error = (format('%s in %s:%s', @reason, @filename, @lineno))
31
+ super(@error)
32
+ end
33
+ end
34
+
35
+ class NgxParserSyntaxError < NgxParserBaseException
36
+ def initialize(filename, lineno, strerror)
37
+ super(filename, lineno, strerror)
38
+ end
39
+ end
40
+
41
+ class NgxParserDirectiveArgumentsError < NgxParserBaseException
42
+ def initialize(filename, lineno, strerror)
43
+ super(filename, lineno, strerror)
44
+ end
45
+ end
46
+
47
+ class NgxParserDirectiveContextError < NgxParserBaseException
48
+ def initialize(filename, lineno, strerror)
49
+ super(filename, lineno, strerror)
50
+ end
51
+ end
52
+
53
+ class NgxParserDirectiveUnknownError < NgxParserBaseException
54
+ def initialize(filename, lineno, strerror)
55
+ super(filename, lineno, strerror)
56
+ end
57
+ end
58
+
59
+ class NgxParserIncludeError < NgxParserBaseException
60
+ def initialize(filename, lineno, strerror)
61
+ super(filename, lineno, strerror)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,42 @@
1
+ require 'crossplane/config'
2
+ require 'crossplane/utils'
3
+
4
+ #require_relative 'config.rb'
5
+ #require_relative 'utils.rb'
6
+
7
+ module CrossPlane
8
+ def self.utils=(utils)
9
+ @utils = utils
10
+ end
11
+
12
+ def self.utils
13
+ @utils
14
+ end
15
+
16
+ def self.config=(config)
17
+ @config = config
18
+ end
19
+
20
+ def self.config
21
+ @config
22
+ end
23
+
24
+ def self.debug=(debug)
25
+ @debug = debug
26
+ end
27
+
28
+ def self.debug
29
+ @debug
30
+ end
31
+
32
+ def self.logger=(logger)
33
+ @logger = logger
34
+ end
35
+
36
+ def self.logger
37
+ @logger || CrossPlane.utils.configure_logger(debug: true)
38
+ end
39
+
40
+ CrossPlane.config = CrossPlane::Config.new()
41
+ CrossPlane.utils = CrossPlane::Utils.new()
42
+ end
@@ -0,0 +1,155 @@
1
+ require 'crossplane/errors'
2
+ require 'crossplane/globals'
3
+
4
+ #require_relative 'errors.rb'
5
+ #require_relative 'globals.rb'
6
+
7
+ module CrossPlane
8
+ class Lexer
9
+ EXTERNAL_LEXERS = {}
10
+ NEWLINE = "\n"
11
+
12
+ attr_accessor :filename
13
+ def initialize(*args)
14
+ args = args[0] || {}
15
+
16
+ required = ['filename']
17
+ conflicts = []
18
+ requires = {}
19
+ valid = {
20
+ 'params' => [
21
+ 'filename',
22
+ ]
23
+ }
24
+
25
+ content = CrossPlane.utils.validate_constructor(client: self, args: args, required: required, conflicts: conflicts, requires: requires, valid: valid)
26
+ self.filename = content[:filename]
27
+ end
28
+
29
+ def lex(*args)
30
+ tokens = _lex_file()
31
+ _balance_braces(tokens)
32
+ tokens
33
+ end
34
+
35
+ private
36
+ def _lex_file(*args)
37
+ token = '' # the token buffer
38
+ next_token_is_directive = true
39
+
40
+ enum = Enumerator.new do |y|
41
+ File.open(self.filename, 'r') { |f|
42
+ f.each do |line|
43
+ lineno = $.
44
+ line.split('').each do |char|
45
+ y.yield [char, lineno]
46
+ end
47
+ end
48
+ }
49
+ end
50
+
51
+ tokens = []
52
+ begin
53
+ while tuple = enum.next
54
+ char, line = tuple
55
+ if CrossPlane.utils.isspace(char)
56
+ if not token.empty?
57
+ tokens.push([token, line])
58
+ if next_token_is_directive and EXTERNAL_LEXERS[token]
59
+ next_token_is_directive = true
60
+ else
61
+ next_token_is_directive = false
62
+ end
63
+ end
64
+
65
+ while CrossPlane.utils.isspace(char)
66
+ char, line = enum.next
67
+ end
68
+
69
+ token = ''
70
+ end
71
+
72
+ # if starting comment
73
+ if token.empty? and char == '#'
74
+ while not char.end_with?(NEWLINE)
75
+ token = token + char
76
+ char, _ = enum.next
77
+ end
78
+ tokens.push([token, line])
79
+ token = ''
80
+ next
81
+ end
82
+
83
+ # handle parameter expansion syntax (ex: "${var[@]}")
84
+ if token and token[-1] == '$' and char == '{'
85
+ next_token_is_directive = false
86
+ while token[-1] != '}' and not CrossPlane.utils.isspace(char)
87
+ token += char
88
+ char, line = enum.next
89
+ end
90
+ end
91
+
92
+ # if a quote is found, add the whole string to the token buffer
93
+ if ['"', "'"].include?(char)
94
+ if not token.empty?
95
+ token = token + char
96
+ next
97
+ end
98
+ quote = char
99
+ char, line = enum.next
100
+ while char != quote
101
+ if char == '\\' + quote
102
+ token = token + quote
103
+ else
104
+ token = token + char
105
+ end
106
+ char, line = enum.next
107
+ end
108
+
109
+ tokens.push([token, line])
110
+ token = ''
111
+ next
112
+ end
113
+
114
+ if ['{', '}', ';'].include?(char)
115
+ if not token.empty?
116
+ tokens.push([token, line])
117
+ token = ''
118
+ end
119
+ tokens.push([char, line]) if char.length > 0
120
+ next_token_is_directive = true
121
+ next
122
+ end
123
+ token = token + char
124
+ end
125
+ rescue StopIteration
126
+ end
127
+ tokens
128
+ end
129
+
130
+ def _balance_braces(tokens)
131
+ depth = 0
132
+
133
+ for token, line in tokens
134
+ if token == '}'
135
+ depth = depth -1
136
+ elsif token == '{'
137
+ depth = depth + 1
138
+ end
139
+
140
+ if depth < 0
141
+ reason = 'unexpected "}"'
142
+ raise CrossPlane::NgxParserSyntaxError.new(self.filename, line, reason)
143
+ else
144
+ yield token, line if block_given?
145
+ end
146
+ end
147
+
148
+ if depth > 0
149
+ reason = 'unexpected end of file, expecting "}"'
150
+ raise CrossPlane::NgxParserSyntaxError.new(self.filename, line, reason)
151
+ end
152
+ end
153
+ end
154
+ end
155
+