log2mail 0.0.1.pre3 → 0.0.1.pre4

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,17 @@
1
+ module Log2mail
2
+ module Config
3
+
4
+ class ConfigFileSnippet
5
+ attr_reader :snippet, :filename
6
+ def initialize( snippet, filename )
7
+ @snippet = snippet
8
+ @filename = filename
9
+ end
10
+
11
+ def to_s
12
+ @snippet
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,220 @@
1
+ require_relative 'attribute'
2
+ require_relative 'section'
3
+
4
+ module Log2mail
5
+ module Config
6
+ class ConfigFileHandler
7
+
8
+ class <<self
9
+ def parse_config(config_path)
10
+ new(config_path)
11
+ # pp config.config
12
+ # config.files.each do |f|
13
+ # puts "File: #{f}"
14
+ # config.patterns_for_file(f).each do |pattern|
15
+ # puts " Pattern: #{pattern}; mailto: " + config.mailtos_for_pattern( f, pattern ).join(', ')
16
+ # end
17
+ # end
18
+ end
19
+ end
20
+
21
+ attr_reader :raw
22
+
23
+ def initialize(config_paths)
24
+ $logger.debug "Reading configuration from #{config_paths}"
25
+ @config_paths = Array(config_paths)
26
+ expand_paths
27
+ @raw = read_configuration
28
+ validate_configuration
29
+ end
30
+
31
+ # returns all the paths of all files needed to be watched
32
+ def files
33
+ @config.keys - [:defaults]
34
+ end
35
+
36
+ def file_patterns
37
+ h = {}
38
+ files.each do |file|
39
+ h[file] = patterns_for_file(file)
40
+ end
41
+ h
42
+ end
43
+
44
+ def patterns_for_file( file )
45
+ Hash(@config[file][:patterns]).keys + \
46
+ Hash(defaults[:patterns]).keys
47
+ end
48
+
49
+ def mailtos_for_pattern( file, pattern )
50
+ m = []
51
+ m.concat Hash( config_file_pattern(file, pattern)[:mailtos] ).keys
52
+ m.concat Hash(Hash(Hash(defaults[:patterns])[pattern])[:mailtos]).keys
53
+ m.concat Array(defaults[:mailtos]) if m.empty?
54
+ m.uniq
55
+ end
56
+
57
+ def settings_for_mailto( file, pattern, mailto )
58
+ h = defaults.reject {|k,v| k==:mailtos}
59
+ h.merge config_file_mailto(file, pattern, mailto)
60
+ end
61
+
62
+ def defaults
63
+ Hash(@config[:defaults])
64
+ end
65
+
66
+ def formatted( show_effective )
67
+ Terminal::Table.new do |t|
68
+ settings_header = show_effective ? 'Effective Settings' : 'Settings'
69
+ t << ['File', 'Pattern', 'Recipient', settings_header]
70
+ t << :separator
71
+ files.each do |file|
72
+ patterns_for_file(file).each do |pattern|
73
+ mailtos_for_pattern(file, pattern).each do |mailto|
74
+ settings = []
75
+ if show_effective
76
+ settings_for_mailto(file, pattern, mailto).each_pair \
77
+ { |k,v| settings << '%s=%s' % [k,v] }
78
+ else
79
+ config_file_mailto(file, pattern, mailto).each_pair \
80
+ { |k,v| settings << '%s=%s' % [k,v] }
81
+ end
82
+ t.add_row [file, pattern, mailto, settings.join($/)]
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def config_file(file)
92
+ Hash(@config[file])
93
+ end
94
+
95
+ def config_file_pattern(file, pattern)
96
+ Hash( Hash( config_file(file)[:patterns] )[pattern] )
97
+ end
98
+
99
+ def config_file_mailtos(file, pattern)
100
+ Hash( config_file_pattern(file, pattern)[:mailtos] )
101
+ end
102
+
103
+ def config_file_mailto(file, pattern, mailto)
104
+ Hash( config_file_mailtos(file, pattern)[mailto] )
105
+ end
106
+
107
+ def expand_paths
108
+ expanded_paths = []
109
+ @config_paths.each do |path|
110
+ if ::File.directory?(path)
111
+ expanded_paths.concat Dir.glob( ::File.join( path, '*[^~#]' ) )
112
+ else
113
+ expanded_paths << path
114
+ end
115
+ end
116
+ @config_paths = expanded_paths
117
+ end
118
+
119
+ # tries to follow original code at https://github.com/lordlamer/log2mail/blob/master/config.cc#L192
120
+ def read_configuration
121
+ @config = {}
122
+ @config_paths.map do |file|
123
+ @section = nil; @pattern = nil; @mailto = nil
124
+ # section, pattern, mailto are reset for every file (but not when included by 'include')
125
+ parse_file( file )
126
+ end.join($/)
127
+ end
128
+
129
+ def parse_file( filename )
130
+ raw = ""
131
+ IO.readlines(filename).each_with_index do |line, lineno|
132
+ raw << line
133
+ parse(filename, line, lineno + 1)
134
+ end
135
+ raw
136
+ rescue Errno::ENOENT
137
+ fail Error, "Configuration file or directory not found (or not readable): #{filename}"
138
+ end
139
+
140
+ def parse(file, line, lineno)
141
+ line.strip!
142
+ return if line =~ /^#/
143
+ return if line =~ /^$/
144
+ line =~ /^(\S+)\s*=?\s*"?(.*?)"?(\s*#.*)?$/ # drop double quotes on right hand side; drop comments
145
+ key, value = $1.to_sym, $2.strip
146
+ if key == :include # include shall work everywhere
147
+ parse_file( ::File.join(Pathname(file).parent, value) )
148
+ return
149
+ end
150
+ if key == :defaults and value.empty? # section: specifies top level; must be 'defaults' or 'file'
151
+ @section = key
152
+ @pattern = nil; @mailto = nil
153
+ fail Error, "Invalid section. Section 'defaults' already specified." if @config[@section]
154
+ @config[@section] = {}
155
+ elsif key == :file
156
+ @section = value
157
+ @pattern = nil; @mailto = nil
158
+ @config[@section] ||= {}
159
+ elsif key == :pattern # must come inside 'file' (or 'defaults')
160
+ # fail "Invalid section. All statements must appear after 'defaults' or 'file=...'" unless @section
161
+ @pattern = value; @mailto = nil
162
+ @config[@section][:patterns] ||= {}
163
+ warning { "Redefining pattern section '#{value}' which has been defined already for '#{@section}'." } \
164
+ if @config[@section][:patterns][value]
165
+ @config[@section][:patterns][value] = {}
166
+ elsif key == :mailto and @section != :defaults # must come inside 'pattern' (or 'defaults')
167
+ fail Error, "'mailto' statements only allowed inside 'pattern' or 'defaults'." unless @pattern
168
+ @mailto = value
169
+ @config[@section][:patterns][@pattern][:mailtos] ||= {}
170
+ warning { "Redefining mailto section '#{value}' which has been defined already for '#{@section}'." } \
171
+ if @config[@section][:patterns][@pattern][:mailtos][value]
172
+ @config[@section][:patterns][@pattern][:mailtos][value] = {}
173
+ else # everything else must come inside 'defaults' or 'mailto'
174
+ fail Error, "'#{key}' must be set within 'defaults' or 'mailto'." unless @section == :defaults or @mailto
175
+ if INT_OPTIONS.include?(key)
176
+ value = value.to_i
177
+ elsif STR_OPTIONS.include?(key)
178
+ elsif PATH_OPTIONS.include?(key)
179
+ value = ::File.expand_path( value, Pathname(file).parent ) unless Pathname(value).absolute?
180
+ elsif key == :mailto # special handling for 'mailto' in 'defaults' section
181
+ @config[:defaults][:mailtos] ||= []
182
+ @config[:defaults][:mailtos] << value
183
+ return # skip the 'mailto' entry itself
184
+ else
185
+ fail Error, "'#{key}' is an unknown configuration statement."
186
+ end
187
+ if @section == :defaults and !@pattern and !@mailto
188
+ warning { "Redefining value for '#{key}'." } if @config[@section][key]
189
+ @config[@section][key] = value
190
+ else
191
+ warning { "Redefining value for '#{key}'." } \
192
+ if @config[@section][:patterns][@pattern][:mailtos][@mailto][key]
193
+ @config[@section][:patterns][@pattern][:mailtos][@mailto][key] = value
194
+ end
195
+ end
196
+ rescue
197
+ fail Error, "#{file} (line #{lineno}): #{$!.message}#$/[#{$!.class} at #{$!.backtrace.first}]"
198
+ end
199
+
200
+ def validate_configuration
201
+ files.each do |file|
202
+ patterns_for_file(file).each do |pattern|
203
+ mailtos = mailtos_for_pattern(file, pattern)
204
+ $logger.warn "Pattern #{file}:#{pattern} has no recipients." if mailtos.empty?
205
+ end
206
+ end
207
+ # FIXME: empty configuration should cause FATAL error
208
+ # TODO: illegal regexp pattern should cause ERROR
209
+ end
210
+
211
+ def warning(&block)
212
+ file = block.binding.eval('file')
213
+ lineno = block.binding.eval('lineno')
214
+ message = block.call
215
+ $logger.warn "#{file} (line #{lineno}): #{message}#$/[at #{caller.first}]"
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,144 @@
1
+ require 'parslet'
2
+
3
+ module Log2mail
4
+ module Config
5
+
6
+ class ParseError < Log2mail::Error
7
+ def initialize( opts )
8
+ @opts = opts
9
+ end
10
+ def original
11
+ @opts[:exception]
12
+ end
13
+ def to_s
14
+ 'File \'' + @opts[:filename] + '\': ' + ($verbose ? original.cause.ascii_tree : original.to_s)
15
+ end
16
+ end
17
+
18
+ class Transform < Parslet::Transform
19
+
20
+ rule(:quoted_string => simple(:value)) { value.to_s.gsub('\\"', '"') }
21
+ rule(:unquoted_string => simple(:value)) { value.to_s.strip }
22
+ rule(:integer => simple(:value)) { value.to_i }
23
+
24
+ rule(:attribute_name => simple(:key), :attribute_value => simple(:value)) do
25
+ Attribute.new( key, value )
26
+ end
27
+
28
+ rule(:section_name => simple(:key), :section_value => simple(:value)) do
29
+ Section.new( key, value )
30
+ end
31
+ rule(:section_name => simple(:key)) do
32
+ Section.new( key )
33
+ end
34
+
35
+ rule(:section => sequence(:values)) { |dict| merge_values( dict[:values] ) }
36
+
37
+ def self.merge_values( values )
38
+ values.reduce do |v1, v2|
39
+ fail "v1 must be a Section" unless v1.instance_of?(Section)
40
+ case v2
41
+ when Attribute
42
+ v1.attrs << v2
43
+ v1
44
+ when Section
45
+ v1.attrs << v2
46
+ v1
47
+ else
48
+ fail "Unsupported value class: #{v1.class}: #{v1.inspect}"
49
+ end
50
+ end
51
+ values.first
52
+ end
53
+
54
+ rule(:section => simple(:section)) do
55
+ fail "Unsupported section class: #{section.class}" unless section.instance_of?(Section)
56
+ section
57
+ end
58
+
59
+ rule(:config => sequence(:sections)) do
60
+ Config.new(sections)
61
+ end
62
+
63
+ rule(:config => simple(:comment)) { Config.new }
64
+
65
+ end
66
+
67
+ class Parser < Parslet::Parser
68
+
69
+ rule(:config) { block.repeat.as(:config) }
70
+ root(:config)
71
+
72
+ rule(:block) { (defaults_section | file_section).as(:section) | eol }
73
+
74
+ rule(:defaults_section) { defaults_section_head >> defaults_section_content }
75
+ rule(:file_section) { file_section_head >> file_section_content }
76
+ rule(:defaults_section_content) { ( mailto_section.as(:section) | pattern_section.as(:section) | attribute | eol ).repeat }
77
+ rule(:file_section_content) { ( pattern_section.as(:section) | attribute | eol ).repeat }
78
+
79
+ rule(:pattern_section) { pattern_section_head >> pattern_section_content }
80
+ rule(:pattern_section_content) { ( mailto_section.as(:section) | attribute | eol ).repeat }
81
+
82
+ rule(:mailto_section) { mailto_section_head >> mailto_section_content }
83
+ rule(:mailto_section_content) { attributes }
84
+
85
+ def line_expression( expr )
86
+ space? >> expr >> eol
87
+ end
88
+
89
+ def equation( name, value )
90
+ line_expression( name >> space? >> str('=') >> space? >> value )
91
+ end
92
+
93
+ rule(:defaults_section_head) { line_expression( str('defaults').as(:section_name) ) }
94
+ rule(:file_section_head) { equation( str('file').as(:section_name), value.as(:section_value)) }
95
+ rule(:mailto_section_head) { equation( str('mailto').as(:section_name), value.as(:section_value)) }
96
+ rule(:pattern_section_head) { equation( str('pattern').as(:section_name), value.as(:section_value)) }
97
+
98
+ rule(:attribute) { equation( valid_attr_name.as(:attribute_name), value.as(:attribute_value) ) }
99
+ # rule(:mailto_attribute) { equation( str('mailto').as(:attribute_name), value.as(:attribute_value)) }
100
+ # rule(:pattern_attribute) { equation( str('pattern').as(:attribute_name), value.as(:attribute_value)) }
101
+
102
+ rule(:attributes) { attribute.repeat }
103
+ rule(:valid_attr_name) { ATTRIBUTES.map{|a| str(a.to_s)}.reduce(&:|) }
104
+
105
+ rule(:value) { quoted_string | integer | string_value }
106
+ rule(:string_value) { (newline.absent? >> comment.absent? >> any).repeat.as(:unquoted_string) }
107
+ rule(:integer) { match['0-9'].repeat(1).as(:integer) }
108
+
109
+ rule(:escaped_quote) { str('\\"') }
110
+ rule(:quoted_string) { quote >> ( escaped_quote | quote.absent? >> any ).repeat.as(:quoted_string) >> quote }
111
+
112
+ rule(:comment) { str('#') >> ( newline.absent? >> any ).repeat }
113
+ rule(:newline) { str("\n") >> str("\r").maybe }
114
+ rule(:quote) { str('"') | str('\'') }
115
+ rule(:space) { match(' ').repeat(1) }
116
+ rule(:space?) { space.maybe }
117
+
118
+ rule(:eol) { space? >> comment.maybe >> newline }
119
+
120
+ def parse_snippets( snippets )
121
+ fail(ArgumentError, "Need ConfigFileSnippets") unless snippets.instance_of?(Array) and snippets.all? {|s| s.instance_of?(ConfigFileSnippet)}
122
+ parsed_tree = {}
123
+ snippets.each do |snippet|
124
+ begin
125
+ parsed_tree.merge! parse(snippet.to_s) do |key, oldval, newval|
126
+ Array(oldval) + Array(newval)
127
+ end
128
+ rescue Parslet::ParseFailed
129
+ fail ParseError.new( filename: snippet.filename, exception: $! )
130
+ end
131
+ end
132
+ Transform.new.apply( parsed_tree )
133
+ end
134
+
135
+ def parse_and_transform( text )
136
+ Transform.new.apply parse(text)
137
+ rescue Parslet::ParseFailed
138
+ raise
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,70 @@
1
+ module Log2mail
2
+ module Config
3
+ class Section
4
+ attr_reader :name, :value
5
+ attr_accessor :attrs
6
+ def initialize( name, value = nil, attrs = [] )
7
+ @name = name.to_sym
8
+ @value = value
9
+ @attrs = attrs
10
+ end
11
+ def ==(other)
12
+ return false unless other.instance_of?(self.class)
13
+ self.name == other.name and self.value == other.value and self.attrs == other.attrs
14
+ end
15
+ def inspect
16
+ 'Section %s = %s---%s' % [@name.inspect, @value.inspect, @attrs.inspect]
17
+ end
18
+ def tree
19
+ h = {}
20
+ @attrs.inject(h) do |h, a|
21
+ if a.instance_of?(Section)
22
+ name = a.name.pluralize.to_sym
23
+ unless a.attrs.empty?
24
+ apdx = {a.value => a.tree}
25
+ else
26
+ apdx = a.value
27
+ end
28
+ case apdx
29
+ when Hash
30
+ apdx.each_pair{ |k,v| ( h[name] ||= {} )[k] = v }
31
+ when Array
32
+ fail "do not know what to do"
33
+ else
34
+ ( h[name] ||= {} )[apdx] = {}
35
+ end
36
+ else
37
+ name = a.name
38
+ h[name] = a.value
39
+ end
40
+ h
41
+ end
42
+ if @name==:file
43
+ return {self.value => h}
44
+ # unless @attrs.empty?
45
+ # return {self.value => h}
46
+ # else
47
+ # return self.value
48
+ # end
49
+ end
50
+ h
51
+ end
52
+
53
+ # merges sections by prefering other's attributes
54
+ # FIXME: needs specing
55
+ def +(other)
56
+ fail "Unmergable sections:\n1) #{self.inspect}\n2) #{other.inspect}\nReason: values must differ." unless self.value == other.value
57
+ @attrs.each do |a|
58
+ case a
59
+ when Attribute
60
+ other.attrs << a unless other.attrs.map(&:name).include?(a.name)
61
+ when Section
62
+ other.attrs << a
63
+ end
64
+ end
65
+ other
66
+ end
67
+
68
+ end
69
+ end
70
+ end