typesafe_config 0.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.
Files changed (40) hide show
  1. data/LICENSE +202 -0
  2. data/README.md +2 -0
  3. data/lib/typesafe/config/config_error.rb +12 -0
  4. data/lib/typesafe/config/config_factory.rb +9 -0
  5. data/lib/typesafe/config/config_object.rb +4 -0
  6. data/lib/typesafe/config/config_parse_options.rb +53 -0
  7. data/lib/typesafe/config/config_render_options.rb +46 -0
  8. data/lib/typesafe/config/config_syntax.rb +7 -0
  9. data/lib/typesafe/config/config_value_type.rb +26 -0
  10. data/lib/typesafe/config/impl/abstract_config_object.rb +64 -0
  11. data/lib/typesafe/config/impl/abstract_config_value.rb +130 -0
  12. data/lib/typesafe/config/impl/config_concatenation.rb +136 -0
  13. data/lib/typesafe/config/impl/config_float.rb +9 -0
  14. data/lib/typesafe/config/impl/config_impl.rb +10 -0
  15. data/lib/typesafe/config/impl/config_impl_util.rb +78 -0
  16. data/lib/typesafe/config/impl/config_int.rb +31 -0
  17. data/lib/typesafe/config/impl/config_number.rb +27 -0
  18. data/lib/typesafe/config/impl/config_string.rb +37 -0
  19. data/lib/typesafe/config/impl/full_includer.rb +4 -0
  20. data/lib/typesafe/config/impl/origin_type.rb +9 -0
  21. data/lib/typesafe/config/impl/parseable.rb +151 -0
  22. data/lib/typesafe/config/impl/parser.rb +882 -0
  23. data/lib/typesafe/config/impl/path.rb +59 -0
  24. data/lib/typesafe/config/impl/path_builder.rb +36 -0
  25. data/lib/typesafe/config/impl/resolve_status.rb +18 -0
  26. data/lib/typesafe/config/impl/simple_config.rb +11 -0
  27. data/lib/typesafe/config/impl/simple_config_list.rb +70 -0
  28. data/lib/typesafe/config/impl/simple_config_object.rb +178 -0
  29. data/lib/typesafe/config/impl/simple_config_origin.rb +174 -0
  30. data/lib/typesafe/config/impl/simple_include_context.rb +7 -0
  31. data/lib/typesafe/config/impl/simple_includer.rb +19 -0
  32. data/lib/typesafe/config/impl/token.rb +32 -0
  33. data/lib/typesafe/config/impl/token_type.rb +42 -0
  34. data/lib/typesafe/config/impl/tokenizer.rb +370 -0
  35. data/lib/typesafe/config/impl/tokens.rb +157 -0
  36. data/lib/typesafe/config/impl/unmergeable.rb +4 -0
  37. data/lib/typesafe/config/impl.rb +5 -0
  38. data/lib/typesafe/config.rb +4 -0
  39. data/lib/typesafe.rb +2 -0
  40. metadata +85 -0
@@ -0,0 +1,370 @@
1
+ require 'typesafe/config/impl'
2
+ require 'typesafe/config/impl/config_impl_util'
3
+ require 'typesafe/config/impl/tokens'
4
+ require 'stringio'
5
+
6
+ class Typesafe::Config::Impl::Tokenizer
7
+ Tokens = Typesafe::Config::Impl::Tokens
8
+
9
+ class TokenizerProblemError < StandardError
10
+ end
11
+
12
+ class TokenIterator
13
+ class WhitespaceSaver
14
+ def initialize
15
+ @whitespace = StringIO.new
16
+ @last_token_was_simple_value = false
17
+ end
18
+
19
+ def add(c)
20
+ if @last_token_was_simple_value
21
+ @whitespace << c
22
+ end
23
+ end
24
+
25
+ def check(t, base_origin, line_number)
26
+ if Typesafe::Config::Impl::Tokenizer::TokenIterator.simple_value?(t)
27
+ next_is_a_simple_value(base_origin, line_number)
28
+ else
29
+ next_is_not_a_simple_value
30
+ nil
31
+ end
32
+ end
33
+
34
+ private
35
+ # called if the next token is not a simple value;
36
+ # discards any whitespace we were saving between
37
+ # simple values.
38
+ def next_is_not_a_simple_value
39
+ @last_token_was_simple_value = false
40
+ @whitespace.reopen("")
41
+ end
42
+
43
+ # called if the next token IS a simple value,
44
+ # so creates a whitespace token if the previous
45
+ # token also was.
46
+ def next_is_a_simple_value(base_origin, line_number)
47
+ if @last_token_was_simple_value
48
+ # need to save whitespace between the two so
49
+ # the parser has the option to concatenate it.
50
+ if @whitespace.length > 0
51
+ Tokens.new_unquoted_text(
52
+ line_origin(base_origin, line_number),
53
+ @whitespace.string = ""
54
+ )
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ # chars JSON allows a number to start with
62
+ FIRST_NUMBER_CHARS = "0123456789-"
63
+ # chars JSON allows to be part of a number
64
+ NUMBER_CHARS = "0123456789eE+-."
65
+ # chars that stop an unquoted string
66
+ NOT_IN_UNQUOTED_TEXT = "$\"{}[]:=,+#`^?!@*&\\"
67
+
68
+ def self.simple_value?(t)
69
+ Tokens.substitution?(t) ||
70
+ Tokens.unquoted_text?(t) ||
71
+ Tokens.value?(t)
72
+ end
73
+
74
+ def self.whitespace?(c)
75
+ Typesafe::Config::Impl::ConfigImplUtil.whitespace?(c)
76
+ end
77
+
78
+ def self.whitespace_not_newline?(c)
79
+ (c != "\n") and (Typesafe::Config::Impl::ConfigImplUtil.whitespace?(c))
80
+ end
81
+
82
+ def initialize(origin, input, allow_comments)
83
+ @origin = origin
84
+ @input = input
85
+ @allow_comments = allow_comments
86
+ @buffer = []
87
+ @line_number = 1
88
+ @line_origin = @origin.set_line_number(@line_number)
89
+ @tokens = []
90
+ @tokens << Tokens::START
91
+ @whitespace_saver = WhitespaceSaver.new
92
+ end
93
+
94
+ def start_of_comment?(c)
95
+ if c == -1
96
+ false
97
+ else
98
+ if @allow_comments
99
+ if c == '#'
100
+ true
101
+ elsif c == '/'
102
+ maybe_second_slash = next_char_raw
103
+ # we want to predictably NOT consume any chars
104
+ put_back(maybe_second_slash)
105
+ if maybe_second_slash == '/'
106
+ true
107
+ else
108
+ false
109
+ end
110
+ end
111
+ else
112
+ false
113
+ end
114
+ end
115
+ end
116
+
117
+ def put_back(c)
118
+ if @buffer.length > 2
119
+ raise ConfigBugError, "bug: putBack() three times, undesirable look-ahead"
120
+ end
121
+ @buffer.push(c)
122
+ end
123
+
124
+ def next_char_raw
125
+ if @buffer.empty?
126
+ begin
127
+ @input.readchar
128
+ rescue EOFError
129
+ -1
130
+ end
131
+ else
132
+ @buffer.pop
133
+ end
134
+ end
135
+
136
+ def next_char_after_whitespace(saver)
137
+ while true
138
+ c = next_char_raw
139
+ if c == -1
140
+ return -1
141
+ else
142
+ if self.class.whitespace_not_newline?(c)
143
+ saver.add(c)
144
+ else
145
+ return c
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # The rules here are intended to maximize convenience while
152
+ # avoiding confusion with real valid JSON. Basically anything
153
+ # that parses as JSON is treated the JSON way and otherwise
154
+ # we assume it's a string and let the parser sort it out.
155
+ def pull_unquoted_text
156
+ origin = @line_origin
157
+ io = StringIO.new
158
+ c = next_char_raw
159
+ while true
160
+ if (c == -1) or
161
+ (NOT_IN_UNQUOTED_TEXT.index(c)) or
162
+ (self.class.whitespace?(c)) or
163
+ (start_of_comment?(c))
164
+ break
165
+ else
166
+ io << c
167
+ end
168
+
169
+ # we parse true/false/null tokens as such no matter
170
+ # what is after them, as long as they are at the
171
+ # start of the unquoted token.
172
+ if io.length == 4
173
+ if io.string == "true"
174
+ return Tokens.new_boolean(origin, true)
175
+ elsif io.string == "null"
176
+ return Tokens.new_null(origin)
177
+ end
178
+ elsif io.length == 5
179
+ if io.string == "false"
180
+ return Tokens.new_boolean(origin, false)
181
+ end
182
+ end
183
+
184
+ c = next_char_raw
185
+ end
186
+
187
+ # put back the char that ended the unquoted text
188
+ put_back(c)
189
+
190
+ Tokens.new_unquoted_text(origin, io.string)
191
+ end
192
+
193
+
194
+ def pull_comment(first_char)
195
+ if first_char == '/'
196
+ discard = next_char_raw
197
+ if discard != '/'
198
+ raise ConfigBugError, "called pullComment but // not seen"
199
+ end
200
+ end
201
+
202
+ io = StringIO.new
203
+ while true
204
+ c = next_char_raw
205
+ if (c == -1) || (c == "\n")
206
+ put_back(c)
207
+ return Tokens.new_comment(@line_origin, io.string)
208
+ else
209
+ io << c
210
+ end
211
+ end
212
+ end
213
+
214
+ def pull_number(first_char)
215
+ sb = StringIO.new
216
+ sb << first_char
217
+ contained_decimal_or_e = false
218
+ c = next_char_raw
219
+ while (c != -1) && (NUMBER_CHARS.index(c))
220
+ if (c == '.') ||
221
+ (c == 'e') ||
222
+ (c == 'E')
223
+ contained_decimal_or_e = true
224
+ end
225
+ sb << c
226
+ c = next_char_raw
227
+ end
228
+ # the last character we looked at wasn't part of the number, put it
229
+ # back
230
+ put_back(c)
231
+ s = sb.string
232
+ begin
233
+ if contained_decimal_or_e
234
+ # force floating point representation
235
+ Tokens.new_double(@line_origin, s.to_f, s)
236
+ else
237
+ Tokens.new_long(@line_origin, s.to_i, s)
238
+ end
239
+ rescue ArgumentError => e
240
+ if e.message =~ /^invalid value for (Float|Integer)\(\)/
241
+ # not a number after all, see if it's an unquoted string.
242
+ s.each do |u|
243
+ if NOT_IN_UNQUOTED_TEXT.index
244
+ raise problem(u, "Reserved character '#{u}'" +
245
+ "is not allowed outside quotes", true)
246
+ end
247
+ end
248
+ # no evil chars so we just decide this was a string and
249
+ # not a number.
250
+ Tokens.new_unquoted_text(@line_origin, s)
251
+ else
252
+ raise e
253
+ end
254
+ end
255
+ end
256
+
257
+ def pull_quoted_string
258
+ # the open quote has already been consumed
259
+ sb = StringIO.new
260
+ c = ""
261
+ while c != '"'
262
+ c = next_char_raw
263
+ if c == -1
264
+ raise problem("End of input but string quote was still open")
265
+ end
266
+
267
+ if c == "\\"
268
+ pull_escape_sequence(sb)
269
+ elsif c == '"'
270
+ # done!
271
+ elsif c =~ /[[:cntrl:]]/
272
+ raise problem(c, "JSON does not allow unescaped #{c}" +
273
+ " in quoted strings, use a backslash escape")
274
+ else
275
+ sb << c
276
+ end
277
+ end
278
+
279
+ # maybe switch to triple-quoted string, sort of hacky...
280
+ if sb.length == 0
281
+ third = next_char_raw
282
+ if third == '"'
283
+ append_triple_quoted_string(sb)
284
+ else
285
+ put_back(third)
286
+ end
287
+ end
288
+
289
+ Tokens.new_string(@line_origin, sb.string)
290
+ end
291
+
292
+ def pull_next_token(saver)
293
+ c = next_char_after_whitespace(saver)
294
+ if c == -1
295
+ Tokens::EOF
296
+ elsif c == "\n"
297
+ # newline tokens have the just-ended line number
298
+ line = Tokens.new_line(@line_origin)
299
+ @line_number += 1
300
+ @line_origin = @origin.set_line_number(@line_number)
301
+ line
302
+ else
303
+ t = nil
304
+ if start_of_comment?(c)
305
+ t = pull_comment(c)
306
+ else
307
+ t = case c
308
+ when '"' then pull_quoted_string
309
+ when '$' then pull_substitution
310
+ when ':' then Tokens::COLON
311
+ when ',' then Tokens::COMMA
312
+ when '=' then Tokens::EQUALS
313
+ when '{' then Tokens::OPEN_CURLY
314
+ when '}' then Tokens::CLOSE_CURLY
315
+ when '[' then Tokens::OPEN_SQUARE
316
+ when ']' then Tokens::CLOSE_SQUARE
317
+ when '+' then pull_plus_equals
318
+ else nil
319
+ end
320
+
321
+ if t.nil?
322
+ if FIRST_NUMBER_CHARS.index(c)
323
+ t = pull_number(c)
324
+ elsif NOT_IN_UNQUOTED_TEXT.index(c)
325
+ raise problem(c, "Reserved character '#{c}' is not allowed outside quotes", true)
326
+ else
327
+ put_back(c)
328
+ t = pull_unquoted_text
329
+ end
330
+ end
331
+ end
332
+
333
+ if t.nil?
334
+ raise ConfigBugError, "bug: failed to generate next token"
335
+ end
336
+
337
+ t
338
+ end
339
+ end
340
+
341
+ def queue_next_token
342
+ t = pull_next_token(@whitespace_saver)
343
+ whitespace = @whitespace_saver.check(t, @origin, @line_number)
344
+ if whitespace
345
+ @tokens.push(whitespace)
346
+ end
347
+ @tokens.push(t)
348
+ end
349
+
350
+ def next
351
+ t = @tokens.shift
352
+ if (@tokens.empty?) and (t != Tokens::EOF)
353
+ begin
354
+ queue_next_token
355
+ rescue TokenizerProblemError => e
356
+ @tokens.push(e.problem)
357
+ end
358
+ if @tokens.empty?
359
+ raise ConfigBugError, "bug: tokens queue should not be empty here"
360
+ end
361
+ end
362
+ t
363
+ end
364
+ end
365
+
366
+
367
+ def self.tokenize(origin, input, syntax)
368
+ TokenIterator.new(origin, input, syntax != Typesafe::Config::ConfigSyntax::JSON)
369
+ end
370
+ end
@@ -0,0 +1,157 @@
1
+ require 'typesafe/config/impl'
2
+ require 'typesafe/config/impl/token'
3
+ require 'typesafe/config/impl/token_type'
4
+ require 'typesafe/config/impl/config_number'
5
+ require 'typesafe/config/impl/config_string'
6
+
7
+ # FIXME the way the subclasses of Token are private with static isFoo and accessors is kind of ridiculous.
8
+ class Typesafe::Config::Impl::Tokens
9
+ Token = Typesafe::Config::Impl::Token
10
+ TokenType = Typesafe::Config::Impl::TokenType
11
+ ConfigNumber = Typesafe::Config::Impl::ConfigNumber
12
+ ConfigString = Typesafe::Config::Impl::ConfigString
13
+
14
+ START = Token.new_without_origin(TokenType::START, "start of file")
15
+ EOF = Token.new_without_origin(TokenType::EOF, "end of file")
16
+ COMMA = Token.new_without_origin(TokenType::COMMA, "','")
17
+ EQUALS = Token.new_without_origin(TokenType::EQUALS, "'='")
18
+ COLON = Token.new_without_origin(TokenType::COLON, "':'")
19
+ OPEN_CURLY = Token.new_without_origin(TokenType::OPEN_CURLY, "'{'")
20
+ CLOSE_CURLY = Token.new_without_origin(TokenType::CLOSE_CURLY, "'}'")
21
+ OPEN_SQUARE = Token.new_without_origin(TokenType::OPEN_SQUARE, "'['")
22
+ CLOSE_SQUARE = Token.new_without_origin(TokenType::CLOSE_SQUARE, "']'")
23
+ PLUS_EQUALS = Token.new_without_origin(TokenType::PLUS_EQUALS, "'+='")
24
+
25
+ class Comment < Token
26
+ def initialize(origin, text)
27
+ super(TokenType::COMMENT, origin)
28
+ @text = text
29
+ end
30
+ attr_reader :text
31
+ end
32
+
33
+ # This is not a Value, because it requires special processing
34
+ class Substitution < Token
35
+ def initialize(origin, optional, expression)
36
+ super(TokenType::SUBSTITUTION, origin)
37
+ @optional = optional
38
+ @value = expression
39
+ end
40
+ end
41
+
42
+ class UnquotedText < Token
43
+ def initialize(origin, s)
44
+ super(TokenType::UNQUOTED_TEXT, origin)
45
+ @value = s
46
+ end
47
+ attr_reader :value
48
+
49
+ def to_s
50
+ "'#{value}'"
51
+ end
52
+ end
53
+
54
+ class Value < Token
55
+ def initialize(value)
56
+ super(TokenType::VALUE, value.origin)
57
+ @value = value
58
+ end
59
+ attr_reader :value
60
+
61
+ def to_s
62
+ "'#{value.unwrapped}' (#{Typesafe::Config::ConfigValueType.name(value.value_type)})"
63
+ end
64
+ end
65
+
66
+ class Line < Token
67
+ def initialize(origin)
68
+ super(TokenType::NEWLINE, origin)
69
+ end
70
+ end
71
+
72
+ class Problem < Token
73
+ def initialize(origin, what, message, suggest_quotes, cause)
74
+ super(TokenType::PROBLEM, origin)
75
+ @what = what
76
+ @message = message
77
+ @suggest_quotes = suggest_quotes
78
+ @cause = cause
79
+ end
80
+ end
81
+
82
+ def self.new_line(origin)
83
+ Line.new(origin)
84
+ end
85
+
86
+ def self.new_comment(origin, text)
87
+ Comment.new(origin, text)
88
+ end
89
+
90
+ def self.new_unquoted_text(origin, s)
91
+ UnquotedText.new(origin, s)
92
+ end
93
+
94
+ def self.new_value(value)
95
+ Value.new(value)
96
+ end
97
+
98
+ def self.new_string(origin, value)
99
+ new_value(ConfigString.new(origin, value))
100
+ end
101
+
102
+ def self.new_long(origin, value, original_text)
103
+ new_value(ConfigNumber.new_number(origin, value, original_text))
104
+ end
105
+
106
+ def self.comment?(t)
107
+ t.is_a?(Comment)
108
+ end
109
+
110
+ def self.comment_text(token)
111
+ if comment?(token)
112
+ token.text
113
+ else
114
+ raise ConfigBugError, "tried to get comment text from #{token}"
115
+ end
116
+ end
117
+
118
+ def self.substitution?(t)
119
+ t.is_a?(Substitution)
120
+ end
121
+
122
+ def self.unquoted_text?(token)
123
+ token.is_a?(UnquotedText)
124
+ end
125
+
126
+ def self.unquoted_text(token)
127
+ if unquoted_text?(token)
128
+ token.value
129
+ else
130
+ raise ConfigBugError, "tried to get unquoted text from #{token}"
131
+ end
132
+ end
133
+
134
+ def self.value?(token)
135
+ token.is_a?(Value)
136
+ end
137
+
138
+ def self.value(token)
139
+ if token.is_a?(Value)
140
+ token.value
141
+ else
142
+ raise ConfigBugError, "tried to get value of non-value token #{token}"
143
+ end
144
+ end
145
+
146
+ def self.value_with_type?(t, value_type)
147
+ value?(t) && (value(t).value_type == value_type)
148
+ end
149
+
150
+ def self.newline?(t)
151
+ t.is_a?(Line)
152
+ end
153
+
154
+ def self.problem?(t)
155
+ t.is_a?(Problem)
156
+ end
157
+ end
@@ -0,0 +1,4 @@
1
+ require 'typesafe/config/impl'
2
+
3
+ module Typesafe::Config::Impl::Unmergeable
4
+ end
@@ -0,0 +1,5 @@
1
+ require 'typesafe/config'
2
+
3
+ module Typesafe::Config::Impl
4
+
5
+ end
@@ -0,0 +1,4 @@
1
+ require 'typesafe'
2
+
3
+ module Typesafe::Config
4
+ end
data/lib/typesafe.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Typesafe
2
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typesafe_config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Price
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-03-14 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ==A port of the Java [Typesafe Config](https://github.com/typesafehub/config)
15
+ library to Ruby
16
+ email: chris@puppetlabs.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/typesafe/config.rb
22
+ - lib/typesafe/config/config_object.rb
23
+ - lib/typesafe/config/config_error.rb
24
+ - lib/typesafe/config/impl/simple_includer.rb
25
+ - lib/typesafe/config/impl/config_number.rb
26
+ - lib/typesafe/config/impl/simple_config.rb
27
+ - lib/typesafe/config/impl/config_int.rb
28
+ - lib/typesafe/config/impl/resolve_status.rb
29
+ - lib/typesafe/config/impl/simple_config_origin.rb
30
+ - lib/typesafe/config/impl/token.rb
31
+ - lib/typesafe/config/impl/config_string.rb
32
+ - lib/typesafe/config/impl/config_float.rb
33
+ - lib/typesafe/config/impl/full_includer.rb
34
+ - lib/typesafe/config/impl/parser.rb
35
+ - lib/typesafe/config/impl/tokens.rb
36
+ - lib/typesafe/config/impl/tokenizer.rb
37
+ - lib/typesafe/config/impl/abstract_config_object.rb
38
+ - lib/typesafe/config/impl/simple_include_context.rb
39
+ - lib/typesafe/config/impl/token_type.rb
40
+ - lib/typesafe/config/impl/path_builder.rb
41
+ - lib/typesafe/config/impl/parseable.rb
42
+ - lib/typesafe/config/impl/path.rb
43
+ - lib/typesafe/config/impl/unmergeable.rb
44
+ - lib/typesafe/config/impl/config_impl_util.rb
45
+ - lib/typesafe/config/impl/simple_config_object.rb
46
+ - lib/typesafe/config/impl/simple_config_list.rb
47
+ - lib/typesafe/config/impl/config_impl.rb
48
+ - lib/typesafe/config/impl/config_concatenation.rb
49
+ - lib/typesafe/config/impl/abstract_config_value.rb
50
+ - lib/typesafe/config/impl/origin_type.rb
51
+ - lib/typesafe/config/config_factory.rb
52
+ - lib/typesafe/config/impl.rb
53
+ - lib/typesafe/config/config_parse_options.rb
54
+ - lib/typesafe/config/config_render_options.rb
55
+ - lib/typesafe/config/config_value_type.rb
56
+ - lib/typesafe/config/config_syntax.rb
57
+ - lib/typesafe.rb
58
+ - LICENSE
59
+ - README.md
60
+ homepage: https://github.com/cprice404/ruby-typesafe-config
61
+ licenses:
62
+ - Apache License, v2
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.25
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Typesafe Config
85
+ test_files: []