lab419-config 0.1

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,2 @@
1
+
2
+ require 'lab419/config/reader'
@@ -0,0 +1,58 @@
1
+ require 'forwardable'
2
+
3
+ module Lab419
4
+ module Config
5
+ class Data
6
+ extend Forwardable
7
+ def_delegators :@store, :empty?
8
+
9
+ def [] key
10
+ @store[ key.to_sym ]
11
+ end
12
+ def []= key, val
13
+ case key
14
+ when String
15
+ store_value val, key.split(".")
16
+ when Symbol
17
+ @store[ key ] = val
18
+ end
19
+ end
20
+ private
21
+ def initialize params={}
22
+ @store = {}
23
+ @overrideable = params[:overrideable]
24
+ end # def initialize
25
+
26
+ def method_missing name, *args, &blk
27
+ if blk.nil? && args.size == 1 && /=\z/ === name then
28
+ return self[ name[0..-2] ] = args.first
29
+ end
30
+
31
+ return super( name, *args, &blk ) unless args.empty?
32
+ @store.fetch( name ){ @store[ name ] = self.class.new( overrideable: @overrideable ) }.
33
+ tap do | ele |
34
+ blk[ ele ] if blk
35
+ end
36
+ end
37
+ def store_value val, keys
38
+ # TODO: This needs HEAVY refactoring
39
+ last_key = keys.pop
40
+ child = parent = @store
41
+ keys.each do | key |
42
+ child = parent[ key.to_sym ]
43
+ case child
44
+ when self.class
45
+ parent = child
46
+ when nil
47
+ parent[ key.to_sym ] = self.class::new
48
+ child = parent = parent[ key.to_sym ]
49
+ else
50
+ raise ArgumentError, "cannot reassign to #{child}"
51
+ end
52
+ end
53
+ raise ArgumentError, "cannot reassign to #{child[last_key]}" if child[last_key]
54
+ child[ last_key.to_sym ] = val
55
+ end
56
+ end # class Data
57
+ end # module Config
58
+ end # module Lab419
@@ -0,0 +1,81 @@
1
+
2
+ module Lab419
3
+ module Config
4
+ IllegalMonitorState = Class::new RuntimeError
5
+
6
+ module Parser
7
+
8
+ SyntaxError = Class::new RuntimeError
9
+
10
+ def parse params={}
11
+ check_source_setup
12
+ @params = params
13
+ @parsed = true
14
+ loop do
15
+ @source.next while @source.current && @source.current =~ comment
16
+ return self if @source.eos?
17
+ key, value = @source.current.split separator_rgx, 2
18
+ raise SyntaxError, "No = found in line \"#{key}\"" unless value
19
+ @config[key.strip] = parse_value value, params
20
+ @source.next
21
+ end
22
+ end
23
+
24
+ private
25
+ def default_handler value
26
+ case value
27
+ when /\A\s*"/
28
+ # TODO: Implement a real string parser
29
+ value.sub(/.*?"/,"").reverse.sub(/\s*"/,"").reverse
30
+ when /\A\s*\[\s*(.*)/
31
+ parse_array $1
32
+ when /\A\s*\{\s*(.*)/
33
+ parse_hash $1
34
+ else
35
+ value.strip
36
+ end
37
+ end
38
+ # HMMM: This might be overkill?
39
+ def invocation handler
40
+ case handler
41
+ when Symbol
42
+ send handler
43
+ when Array
44
+ handler.next.send( @source, *handler )
45
+ when Proc
46
+ handler[ @source ]
47
+ else
48
+ raise IllegalHandlerSpec, "#{handler} is neither a sym, array or proc"
49
+ end
50
+ end
51
+ def parse_array current_element
52
+ result = current_element.strip.empty? ? [] : [ parse_value( current_element ) ]
53
+ while line = @source.next do
54
+ if line =~ /\A\s*\]/ then
55
+ return result
56
+ end
57
+ result << parse_value( line )
58
+ end
59
+ raise SyntaxError, "missing ]"
60
+ end
61
+ def parse_hash current_pair
62
+ raise SyntaxError, "not yet implemented"
63
+ end
64
+ def parse_value value, params={}
65
+ @handlers.update( params.fetch( :handlers, {} ) ).
66
+ each do | trigger_rgx, handler |
67
+ if value =~ trigger_rgx then
68
+ return invocation( handler )
69
+ end
70
+ end
71
+ return default_handler( value )
72
+ end
73
+
74
+ def check_source_setup
75
+ raise Lab419::Config::IllegalMonitorState, "source not set up, either call new with a string source or set the source up with
76
+ one of the with_* methods" unless @source
77
+
78
+ end
79
+ end # module Parser
80
+ end # module Config
81
+ end # module Lab419
@@ -0,0 +1,80 @@
1
+
2
+ require 'lab419/config/data'
3
+ require 'lab419/config/source'
4
+ require 'lab419/config/parser'
5
+
6
+ module Lab419
7
+ module Config
8
+ IllegalHandlerSpec = Class::new RuntimeError
9
+ DataError = Class::new RuntimeError
10
+ # TODO: code @handler setters, @handler is a dead variable right now
11
+ # no specs imply its usage yet
12
+ class Reader
13
+ include Parser
14
+
15
+ attr_reader :config, :name
16
+
17
+ def parsed?; @parsed end
18
+
19
+ def handle trigger_rgx, message_name = nil, &blk
20
+ if message_name && blk || blk.nil? && message_name.nil?
21
+ raise ArgumentError, "please specify a message_name *or* a block, not both or none"
22
+ end
23
+ @handlers.update( trigger_rgx => message_name || blk )
24
+ end
25
+ def with_file filename, &blk
26
+ File.open( filename ) do | f |
27
+ init_source f, filename
28
+ end
29
+ blk[self] if blk
30
+ self
31
+ rescue Errno::ENOENT
32
+ raise DataError, "hmm file: '#{filename}' does not seem to be readable!"
33
+ end
34
+
35
+ def with_string string, &blk
36
+ init_source string
37
+ blk[self] if blk
38
+ self
39
+ end
40
+
41
+ private
42
+ def comment
43
+ %r{\A\s*#|\A\s*\z}
44
+ end
45
+ def separator_rgx
46
+ @params.fetch( :separator, %r{\s*=} )
47
+ end
48
+ def initialize input=nil
49
+ @config = Data::new
50
+ @params = {}
51
+ @source = nil
52
+ @parsed = false
53
+ init_source input if input
54
+ init_handlers
55
+ end
56
+
57
+ def init_handlers
58
+ @handlers={}
59
+ end
60
+
61
+ def init_source input, name=nil
62
+ source =
63
+ case input
64
+ when String
65
+ input.split( %r{\r?\n} )
66
+ when Array
67
+ input
68
+ when IO
69
+ input.readlines.map( &:chomp )
70
+ end
71
+ @name = name
72
+ @source = Lab419::Config::Source::new( source, name )
73
+ end # def init_source
74
+
75
+ def syntax_error msg=""
76
+ raise SyntaxError, msg
77
+ end
78
+ end # class Reader
79
+ end # module Config
80
+ end # module Lab419
@@ -0,0 +1,56 @@
1
+
2
+ module Lab419
3
+ module Config
4
+ #
5
+ # Source is a wrapper arround an array of lines, adding the ability to
6
+ # store the current line_number #line_nb and the name of the source #name.
7
+ # It implements a minimal interface, allowing to advance line by line via
8
+ # the #next method, accessing the current element via #current and testing
9
+ # for the end of the array via the #eos? method
10
+ #
11
+ class Source
12
+
13
+ attr_reader :name
14
+
15
+ #
16
+ # Readers
17
+ #
18
+
19
+ #
20
+ # accesses the current element
21
+ def current
22
+ @lines[ @cursor ]
23
+ end
24
+ #
25
+ # true iff #next has advanced over the last line
26
+ def eos?
27
+ @lines[ @cursor ].nil?
28
+ end
29
+ #
30
+ # current line number, starting with 1, incremented by each
31
+ # call of #next, up to a maximum of the size of the underlying
32
+ # array.
33
+ def line_nb
34
+ [ @cursor + 1, @lines.length ].min
35
+ end
36
+
37
+ # This is the *only* modifier of Source, it
38
+ # advances to the next line of the underlying array.
39
+ # The operation is irreversible.
40
+ def next
41
+ @cursor += 1
42
+ current
43
+ end
44
+
45
+ def to_debug
46
+ "#{@lines[@cursor-1]}:>#{current}<:#{@lines[@cursor+1]}"
47
+ end
48
+ private
49
+ def initialize lines, name=nil
50
+ @lines = lines
51
+ @cursor = 0
52
+ @name = name || "<annon>"
53
+ end
54
+ end # class Source
55
+ end # module Config
56
+ end # module Lab419
@@ -0,0 +1,68 @@
1
+
2
+ $:.unshift File.join( File.dirname( __FILE__ ), %w{ .. lib } )
3
+ require 'lab419/config/data'
4
+
5
+ describe Lab419::Config::Data do
6
+ before :each do
7
+ @data = Lab419::Config::Data::new
8
+ end
9
+
10
+ it "shall not have any data [Symbol]" do
11
+ @data[:x].should be_nil
12
+ end
13
+ it "shall not have any data Method" do
14
+ @data.x.should be_empty
15
+ end
16
+ it "shall recoginze missing methods with params" do
17
+ lambda{ @data.x(42) }.should raise_error( NoMethodError )
18
+ end # it "shall recoginze all other method formats as missing methods"
19
+ it "shall recoginze missing methods with params and blocks" do
20
+ lambda{ @data.x(42){42} }.should raise_error( NoMethodError )
21
+ end # it "shall recoginze all other method formats as missing methods"
22
+ it "shall not have any data [String]" do
23
+ @data["x"].should be_nil
24
+ end
25
+
26
+ it "shall store values for simple keys" do
27
+ @data["x"] = 42
28
+ @data["x"].should == 42
29
+ end
30
+ it "shall store values for simple keys being Str/Sym agnostic" do
31
+ @data["x"] = 42
32
+ @data[:x].should == 42
33
+ end
34
+
35
+ describe "compund keys" do
36
+ before :each do
37
+ @data["a.b.c"] = 41
38
+ end # before :each
39
+
40
+ it "shall intercept the compound key" do
41
+ @data['a.b.c'].should be_nil
42
+ end # it "shall intercept the compound key"
43
+
44
+ it "shall give data objects for intermediates" do
45
+ @data['a'].should be_kind_of( @data.class )
46
+ end # it "shall give data objects for intermediates"
47
+
48
+ it "shall give scalars for final objects" do
49
+ @data['a'].b[:c].should == 41
50
+ end # it "shall give scalars for final objects"
51
+
52
+ end # describe "compund keys"
53
+
54
+ describe "autoassignment via method" do
55
+ before :each do
56
+ @data.x = 42
57
+ @data.alpha.beta do | d |
58
+ d.gamma = 41
59
+ end
60
+ end
61
+ it "should have the key" do
62
+ @data[:x].should == 42
63
+ end
64
+ it "should have the compound key" do
65
+ @data[:alpha].beta.gamma.should == 41
66
+ end
67
+ end
68
+ end # describe Lab419::Config::Data
@@ -0,0 +1,133 @@
1
+
2
+ #require 'config_spec_helper'
3
+ $:.unshift File.join( File.dirname( __FILE__ ), %w{ .. lib } )
4
+ require 'lab419/config'
5
+
6
+ describe Lab419::Config::Reader do
7
+
8
+ before :all do
9
+ @cr = Lab419::Config::Reader::new %{
10
+ a=42
11
+ b = 43
12
+ c.d = alpha
13
+ e = " "
14
+ some.list = [ e1
15
+ e2
16
+ e3
17
+ ]
18
+ empty.list = [
19
+ ]
20
+ notha = [
21
+ 1
22
+ 2
23
+ 3
24
+ ]
25
+ }
26
+ @c = @cr.parse.config
27
+ end
28
+
29
+ describe "should contain correct values" do
30
+ describe "scalars" do
31
+ it "shall contain :a == 42" do
32
+ @c[:a].should == "42"
33
+ end
34
+ it "shall contain a == 42" do
35
+ @c["a"].should == "42"
36
+ end
37
+ it "shall contain .a == 42" do
38
+ @c.a.should == "42"
39
+ end
40
+ it "shall contain b" do
41
+ @c.b.should == "43"
42
+ end
43
+ it "shall contain subobjects" do
44
+ @c.c["d"].should == "alpha"
45
+ end
46
+ it "shall contain spaces" do
47
+ @c.e.should == " "
48
+ end
49
+ end
50
+ describe "arrays" do
51
+ it "should have e1 to e3" do
52
+ @c.some.list.should == %w{ e1 e2 e3 }
53
+ end
54
+ it "should have an empty list" do
55
+ @c[:empty].list.should be_empty
56
+ end # it "should have an empty list"
57
+ it "1,2,3" do
58
+ @c.notha.should == (1..3).map(&:to_s)
59
+ end # it "1,2,3"
60
+ end
61
+ end # describe "should contain correct values"
62
+ describe "can customize the assignment syntax" do
63
+ before :all do
64
+ @cr = Lab419::Config::Reader::new %{
65
+ a: 42
66
+ b : true
67
+ }
68
+ @d = @cr.parse separator: /\s*:/
69
+ end
70
+ it "shall see the values" do
71
+ @d.config.a.should == "42"
72
+ end # it "shall see the"
73
+ end # describe "can customize the assignment syntax"
74
+ describe "can use custom handlers" do
75
+ before :all do
76
+ @cr = Lab419::Config::Reader::new %w{
77
+ a = 42
78
+ b.c = nil
79
+ d = {
80
+ a, b,
81
+ c
82
+ }
83
+ }
84
+ end
85
+ end # describe "can use custom handlers"
86
+ end # describe Lab419::ConfigReader
87
+
88
+ def should_parse_correctly
89
+ @cr.config.a.should == '42'
90
+ end
91
+ describe "how to provide data" do
92
+ before :all do
93
+ File.open( "xxx", "w" ) do |f|
94
+ f.puts "a = 42"
95
+ end
96
+ end
97
+ before :each do
98
+ @cr = Lab419::Config::Reader::new
99
+ end
100
+
101
+ after :all do
102
+ File.unlink "xxx"
103
+ end
104
+
105
+ it "shall not parse without a source" do
106
+ lambda{ @cr.parse }.should raise_error( Lab419::Config::IllegalMonitorState )
107
+ end # it "shall not parse without a source"
108
+ it "shall parse with a string source" do
109
+ @cr.with_string( "a=42" ).parse
110
+ should_parse_correctly
111
+ end
112
+ it "shall parse with a file source" do
113
+ @cr.with_file "xxx"
114
+ @cr.parse
115
+ should_parse_correctly
116
+ end
117
+ it "shall have a correct name" do
118
+ @cr.with_file "xxx" do | r |
119
+ r.parse
120
+ r.config[:a].should == "42"
121
+ end
122
+ @cr.parsed?.should == true
123
+ end
124
+ it "shall have access to the name" do
125
+ @cr.with_file "xxx" do | r |
126
+ r.name.should == "xxx"
127
+ end # with_file "xxx"
128
+ end
129
+ it "shall raise a DataError if provided file does not exist" do
130
+ lambda{ @cr.with_file "yyy" }.should raise_error( Lab419::Config::DataError )
131
+ end # it "shall raise a DataError if provided file does not exist"
132
+ end
133
+