infer-type 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/infer_type/parser.rb +39 -0
- data/lib/infer_type/parsers/date.rb +48 -0
- data/lib/infer_type/parsers/false.rb +31 -0
- data/lib/infer_type/parsers/float.rb +61 -0
- data/lib/infer_type/parsers/integer.rb +63 -0
- data/lib/infer_type/parsers/nil.rb +31 -0
- data/lib/infer_type/parsers/time.rb +116 -0
- data/lib/infer_type/parsers/true.rb +31 -0
- data/lib/infer_type/registry.rb +104 -0
- data/lib/infer_type/result.rb +26 -0
- data/lib/infer_type.rb +99 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: d42e183dada37127aec852bd61db28217f0f3cd2
|
|
4
|
+
data.tar.gz: 4d7944e9b781eb03cdc2ecd60babec1a76c2394c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6bc5121ea29c3fbd4b5002f1f46d6a25a713edbc730c219766125e54747ddb730b1bc2a6cbd7bcca9871be704cd1f205abc9781751f47a6975df1ebdd8f0075a
|
|
7
|
+
data.tar.gz: 01a7fd3195f626a76c21e568d5bab630b6c01f5bd60e29ca1f792819b7e6502bed5ac7614d3cef1419e7509b07809dcfe1e51fe4a665974cbd0a6dbb0eeee023
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
# Base class for parsers
|
|
5
|
+
class Parser
|
|
6
|
+
class << self
|
|
7
|
+
# Method to specify or retrieve the type this parser detects
|
|
8
|
+
def detects(type = nil)
|
|
9
|
+
if type
|
|
10
|
+
raise ArgumentError, "detects cannot be String" if type == String
|
|
11
|
+
raise ArgumentError, "detects must be a Class" unless type.is_a?(Class)
|
|
12
|
+
@detects = type
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@detects
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Subclasses must implement this method
|
|
20
|
+
def parse(_str)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Methods to create success and failure results
|
|
27
|
+
def success(value)
|
|
28
|
+
InferType::Success.new(
|
|
29
|
+
value: value,
|
|
30
|
+
parser: self.class
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure
|
|
35
|
+
InferType::Failure.new(parser: self.class)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module InferType
|
|
6
|
+
module Parsers
|
|
7
|
+
class DateParser < InferType::Parser
|
|
8
|
+
detects Date
|
|
9
|
+
|
|
10
|
+
DATE_FORMAT = %r<
|
|
11
|
+
\A
|
|
12
|
+
(?<date>
|
|
13
|
+
(?<year> \d{4} ) -
|
|
14
|
+
(?<month> [0-1]\d ) -
|
|
15
|
+
(?<day> [0-3]\d )
|
|
16
|
+
)
|
|
17
|
+
\z
|
|
18
|
+
>x.freeze
|
|
19
|
+
|
|
20
|
+
def parse(str)
|
|
21
|
+
candidate = str.strip
|
|
22
|
+
return failure if candidate.empty?
|
|
23
|
+
|
|
24
|
+
# Looks like a Date
|
|
25
|
+
return failure unless match = candidate.match(DATE_FORMAT)
|
|
26
|
+
|
|
27
|
+
# Pre-validate to avoid Date.new being too lax
|
|
28
|
+
|
|
29
|
+
# Attempt to parse the date
|
|
30
|
+
year = match['year'].to_i
|
|
31
|
+
month = match['month'].to_i
|
|
32
|
+
day = match['day'].to_i
|
|
33
|
+
return failure unless Date.valid_date?(year, month, day)
|
|
34
|
+
|
|
35
|
+
# Attempt conversion
|
|
36
|
+
begin
|
|
37
|
+
value = Date.new(year, month, day)
|
|
38
|
+
return failure unless value.year == year && value.month == month && value.day == day
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
return failure
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
success(value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
module Parsers
|
|
5
|
+
class FalseParser < InferType::Parser
|
|
6
|
+
detects FalseClass
|
|
7
|
+
|
|
8
|
+
FALSE_STRINGS = ['false', 'no', 'off', 'n'].freeze
|
|
9
|
+
|
|
10
|
+
# If true, only use the first false string
|
|
11
|
+
STRICT = true
|
|
12
|
+
|
|
13
|
+
# If true, case sensitive
|
|
14
|
+
CASE_SENSITIVE = false
|
|
15
|
+
|
|
16
|
+
def parse(str)
|
|
17
|
+
candidate = str.strip
|
|
18
|
+
candidate = candidate.downcase if !CASE_SENSITIVE
|
|
19
|
+
return failure if candidate.empty?
|
|
20
|
+
|
|
21
|
+
if STRICT
|
|
22
|
+
return success(false) if candidate == FALSE_STRINGS.first
|
|
23
|
+
else
|
|
24
|
+
return success(false) if FALSE_STRINGS.include?(candidate)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
failure
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
module Parsers
|
|
5
|
+
class FloatParser < InferType::Parser
|
|
6
|
+
detects Float
|
|
7
|
+
|
|
8
|
+
# If false, "1" is NOT considered a Float (must be "1.0" etc.)
|
|
9
|
+
ALLOW_INTEGER = true
|
|
10
|
+
|
|
11
|
+
# Allow exponential notation like 1e3 or 1.5E-2
|
|
12
|
+
ALLOW_EXPONENTIAL = true
|
|
13
|
+
|
|
14
|
+
# Allow special values: NaN, Infinity
|
|
15
|
+
ALLOW_SPECIAL_VALUES = false
|
|
16
|
+
|
|
17
|
+
FLOAT_NO_EXP_REGEX = /\A[+-]?(?:\d+\.\d*|\.\d+)\z/
|
|
18
|
+
FLOAT_WITH_EXP_REGEX = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)[eE][+-]?\d+\z/
|
|
19
|
+
|
|
20
|
+
def parse(str)
|
|
21
|
+
candidate = str.strip
|
|
22
|
+
return failure if candidate.empty?
|
|
23
|
+
|
|
24
|
+
# Check for exact special value string
|
|
25
|
+
if ["NaN", "Infinity", "-Infinity"].include?(candidate)
|
|
26
|
+
if ALLOW_SPECIAL_VALUES
|
|
27
|
+
return success(Float(candidate))
|
|
28
|
+
else
|
|
29
|
+
return failure
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check for integer if allowed
|
|
34
|
+
if ALLOW_INTEGER
|
|
35
|
+
InferType::Parsers::IntegerParser.new.parse(candidate).tap do |result|
|
|
36
|
+
if result.parsed
|
|
37
|
+
# Return it as a float
|
|
38
|
+
return success(result.value.to_f)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check for float format
|
|
44
|
+
looks_float_no_exp = !candidate.match(FLOAT_NO_EXP_REGEX).nil?
|
|
45
|
+
looks_float_with_exp = ALLOW_EXPONENTIAL && !candidate.match(FLOAT_WITH_EXP_REGEX).nil?
|
|
46
|
+
looks_float = looks_float_no_exp || looks_float_with_exp
|
|
47
|
+
|
|
48
|
+
return failure unless looks_float
|
|
49
|
+
|
|
50
|
+
# Attempt conversion
|
|
51
|
+
begin
|
|
52
|
+
value = Float(candidate)
|
|
53
|
+
rescue ArgumentError
|
|
54
|
+
return failure
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
success(value)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
module Parsers
|
|
5
|
+
class IntegerParser < InferType::Parser
|
|
6
|
+
detects Integer
|
|
7
|
+
|
|
8
|
+
# Allow numbers with leading zeros like "00001" = 1
|
|
9
|
+
ALLOW_LEADING_ZERO = true
|
|
10
|
+
|
|
11
|
+
# Allow leading plus sign
|
|
12
|
+
ALLOW_LEADING_PLUS = true
|
|
13
|
+
|
|
14
|
+
INTEGER_REGEX = /\A-?\d+\z/
|
|
15
|
+
|
|
16
|
+
def parse(str)
|
|
17
|
+
candidate = str.strip
|
|
18
|
+
return failure if candidate.empty?
|
|
19
|
+
|
|
20
|
+
# Prepare the candidate for parsing
|
|
21
|
+
candidate = clean(candidate)
|
|
22
|
+
|
|
23
|
+
# Should now match integer format
|
|
24
|
+
return failure unless !candidate.match(INTEGER_REGEX).nil?
|
|
25
|
+
|
|
26
|
+
# Attempt conversion
|
|
27
|
+
begin
|
|
28
|
+
value = Integer(candidate, 10)
|
|
29
|
+
return failure unless value.to_s == candidate
|
|
30
|
+
rescue ArgumentError
|
|
31
|
+
return failure
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
success(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def clean(str)
|
|
40
|
+
clean = str.dup.strip
|
|
41
|
+
return clean if clean.empty?
|
|
42
|
+
|
|
43
|
+
# Remove leading +
|
|
44
|
+
if ALLOW_LEADING_PLUS
|
|
45
|
+
clean = clean.sub(/\A\+/, '')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Preserve a single zero when the string is all zeros
|
|
49
|
+
if !clean.match(/\A-?0+\z/).nil?
|
|
50
|
+
return '0' # -0 is equivalent to 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove leading zeros
|
|
54
|
+
if ALLOW_LEADING_ZERO
|
|
55
|
+
clean = clean.sub(/\A(-)?0+/, '\1')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
clean
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
module Parsers
|
|
5
|
+
class NilParser < InferType::Parser
|
|
6
|
+
detects NilClass
|
|
7
|
+
|
|
8
|
+
NIL_STRINGS = ['nil', 'null', 'nothing', 'undefined', 'none'].freeze
|
|
9
|
+
|
|
10
|
+
# If true, only use the first nil string
|
|
11
|
+
STRICT = true
|
|
12
|
+
|
|
13
|
+
# If true, case sensitive
|
|
14
|
+
CASE_SENSITIVE = false
|
|
15
|
+
|
|
16
|
+
def parse(str)
|
|
17
|
+
candidate = str.strip
|
|
18
|
+
candidate = candidate.downcase if !CASE_SENSITIVE
|
|
19
|
+
return failure if candidate.empty?
|
|
20
|
+
|
|
21
|
+
if STRICT
|
|
22
|
+
return success(nil) if candidate == NIL_STRINGS.first
|
|
23
|
+
else
|
|
24
|
+
return success(nil) if NIL_STRINGS.include?(candidate)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
failure
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module InferType
|
|
7
|
+
module Parsers
|
|
8
|
+
class TimeParser < InferType::Parser
|
|
9
|
+
detects Time
|
|
10
|
+
|
|
11
|
+
# If timezone offset is missing from a Time string, assume UTC
|
|
12
|
+
# If false, the system's local timezone will be used
|
|
13
|
+
DEFAULT_UTC = true
|
|
14
|
+
|
|
15
|
+
TIME_FORMAT = %r<
|
|
16
|
+
\A
|
|
17
|
+
(?<date>
|
|
18
|
+
(?<year> \d{4} ) -
|
|
19
|
+
(?<month> [0-1]\d ) -
|
|
20
|
+
(?<day> [0-3]\d )
|
|
21
|
+
)
|
|
22
|
+
(T|\s)
|
|
23
|
+
(?<time>
|
|
24
|
+
(?<hour> [0-2]\d ) \:
|
|
25
|
+
(?<minute> [0-5]\d )
|
|
26
|
+
(\:
|
|
27
|
+
(?<second> [0-5]\d )
|
|
28
|
+
([\.,]
|
|
29
|
+
(?<subs> \d{1,9} )
|
|
30
|
+
)?
|
|
31
|
+
)?
|
|
32
|
+
)
|
|
33
|
+
(?<offset>
|
|
34
|
+
(?<sign> [+\-] )
|
|
35
|
+
(?<offh> [0-2]\d ) \:?
|
|
36
|
+
(?<offm> [0-5]\d )
|
|
37
|
+
|
|
|
38
|
+
(?<offz> Z | \s?UTC )
|
|
39
|
+
)?
|
|
40
|
+
\z
|
|
41
|
+
>x.freeze
|
|
42
|
+
|
|
43
|
+
def parse(str)
|
|
44
|
+
candidate = str.strip
|
|
45
|
+
return failure if candidate.empty?
|
|
46
|
+
|
|
47
|
+
# Looks like a Time
|
|
48
|
+
return failure unless match = candidate.match(TIME_FORMAT)
|
|
49
|
+
|
|
50
|
+
# Pre-validate to avoid Time.new being too lax
|
|
51
|
+
|
|
52
|
+
# Attempt to parse date
|
|
53
|
+
year = match['year'].to_i
|
|
54
|
+
month = match['month'].to_i
|
|
55
|
+
day = match['day'].to_i
|
|
56
|
+
return failure unless Date.valid_date?(year, month, day)
|
|
57
|
+
|
|
58
|
+
# Attempt to parse time
|
|
59
|
+
hour = match['hour'].to_i
|
|
60
|
+
minute = match['minute'].to_i
|
|
61
|
+
return failure unless hour.between?(0, 23) && minute.between?(0, 59)
|
|
62
|
+
if match['second']
|
|
63
|
+
second = match['second'].to_i
|
|
64
|
+
return failure unless second.between?(0, 59)
|
|
65
|
+
else
|
|
66
|
+
second = 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Add on subseconds if they were present
|
|
70
|
+
if match['subs']
|
|
71
|
+
begin
|
|
72
|
+
subs = ("0." + match['subs']).to_f
|
|
73
|
+
second += subs
|
|
74
|
+
rescue ArgumentError
|
|
75
|
+
return failure
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Was there an offset?
|
|
80
|
+
if match['offset'] && !match['offz']
|
|
81
|
+
sign = match['sign']
|
|
82
|
+
offh = match['offh'].to_i
|
|
83
|
+
offm = match['offm'].to_i
|
|
84
|
+
return failure unless (offh.between?(0, 13) && offm.between?(0, 59)) || (offh == 14 && offm == 0) # maximum 14 hour offsets
|
|
85
|
+
else
|
|
86
|
+
if DEFAULT_UTC || match['offz']
|
|
87
|
+
sign = "+"
|
|
88
|
+
offh = 0
|
|
89
|
+
offm = 0
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
if sign && offh && offm
|
|
93
|
+
offset = format("%<sign>s%<offh>02d:%<offm>02d", sign: sign, offh: offh, offm: offm)
|
|
94
|
+
else
|
|
95
|
+
offset = nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Attempt conversion
|
|
99
|
+
begin
|
|
100
|
+
args = [year, month, day, hour, minute, second]
|
|
101
|
+
args << offset if offset
|
|
102
|
+
value = Time.new(*args)
|
|
103
|
+
return failure unless value.year == year && value.mon == month && value.day == day
|
|
104
|
+
return failure unless value.hour == hour && value.min == minute
|
|
105
|
+
return failure unless second.to_r == value.sec + value.subsec
|
|
106
|
+
return failure unless value.utc_offset == (offh * 3600 + offm * 60) * (sign == "-" ? -1 : 1) if offset
|
|
107
|
+
rescue ArgumentError
|
|
108
|
+
return failure
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
success(value)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
module Parsers
|
|
5
|
+
class TrueParser < InferType::Parser
|
|
6
|
+
detects TrueClass
|
|
7
|
+
|
|
8
|
+
TRUE_STRINGS = ['true', 'yes', 'on', 'y'].freeze
|
|
9
|
+
|
|
10
|
+
# If true, only use the first true string
|
|
11
|
+
STRICT = true
|
|
12
|
+
|
|
13
|
+
# If true, case sensitive
|
|
14
|
+
CASE_SENSITIVE = false
|
|
15
|
+
|
|
16
|
+
def parse(str)
|
|
17
|
+
candidate = str.strip
|
|
18
|
+
candidate = candidate.downcase if !CASE_SENSITIVE
|
|
19
|
+
return failure if candidate.empty?
|
|
20
|
+
|
|
21
|
+
if STRICT
|
|
22
|
+
return success(true) if candidate == TRUE_STRINGS.first
|
|
23
|
+
else
|
|
24
|
+
return success(true) if TRUE_STRINGS.include?(candidate)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
failure
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize
|
|
6
|
+
@parsers = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parsers
|
|
10
|
+
@parsers.dup
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parser_for_type(type)
|
|
14
|
+
@parsers.find { |parser| parser.detects == type }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Methods to manage parser registration
|
|
18
|
+
def register(parser_class)
|
|
19
|
+
validate_parser_class!(parser_class)
|
|
20
|
+
|
|
21
|
+
type = parser_class.detects
|
|
22
|
+
existing_index = @parsers.index { |parser| parser.detects == type }
|
|
23
|
+
|
|
24
|
+
if existing_index
|
|
25
|
+
old = @parsers[existing_index]
|
|
26
|
+
@parsers[existing_index] = parser_class
|
|
27
|
+
return old
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Last registered has lowest priority
|
|
31
|
+
@parsers << parser_class
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def deregister(parser_classes)
|
|
36
|
+
Array(parser_classes).each do |klass|
|
|
37
|
+
@parsers.delete(klass)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Methods to manage parser priority
|
|
42
|
+
def prioritize(*items)
|
|
43
|
+
reorder(items, to_front: true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def deprioritize(*items)
|
|
47
|
+
reorder(items, to_front: false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Ensure parser class being registered is valid
|
|
53
|
+
def validate_parser_class!(parser_class)
|
|
54
|
+
unless parser_class.is_a?(Class) && parser_class < InferType::Parser
|
|
55
|
+
raise ArgumentError, "Parser must subclass InferType::Parser"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
type = parser_class.detects
|
|
59
|
+
raise ArgumentError, "Parser must declare the type it detects" if type.nil?
|
|
60
|
+
|
|
61
|
+
unless type.is_a?(Class) && !type.is_a?(String)
|
|
62
|
+
raise ArgumentError, "Parser must detect a Class (other than String)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
unless parser_class.instance_methods(false).include?(:parse)
|
|
66
|
+
raise ArgumentError, "Parser must define #parse(str)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
arity = parser_class.instance_method(:parse).arity
|
|
70
|
+
unless arity == 1 || arity == -2
|
|
71
|
+
raise ArgumentError, "#parse must accept exactly one argument"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Normalize parser class or type to parser class
|
|
76
|
+
def normalize(items)
|
|
77
|
+
Array(items).map do |item|
|
|
78
|
+
if item.is_a?(Class) && item < InferType::Parser
|
|
79
|
+
item
|
|
80
|
+
elsif item.is_a?(Class)
|
|
81
|
+
parser = parser_for_type(item)
|
|
82
|
+
raise KeyError, "No parser registered for type #{item}" if parser.nil?
|
|
83
|
+
parser
|
|
84
|
+
else
|
|
85
|
+
raise ArgumentError, "Expected parser class or type class"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Reorder parsers to front or back
|
|
91
|
+
def reorder(items, to_front:)
|
|
92
|
+
targets = normalize(items)
|
|
93
|
+
remaining = @parsers.reject { |p| targets.include?(p) }
|
|
94
|
+
|
|
95
|
+
@parsers =
|
|
96
|
+
if to_front
|
|
97
|
+
targets + remaining
|
|
98
|
+
else
|
|
99
|
+
remaining + targets
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InferType
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :parsed, :value, :parser
|
|
6
|
+
def initialize(parsed: nil, value: nil, parser: nil)
|
|
7
|
+
@parsed = parsed
|
|
8
|
+
@value = value
|
|
9
|
+
@parser = parser
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Success < Result
|
|
14
|
+
def initialize(value: nil, parser: nil)
|
|
15
|
+
raise TypeError, "Parser returned a type other than the type it detects" if !value.is_a?(parser.detects)
|
|
16
|
+
super(parsed: true, value: value, parser: parser)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Failure < Result
|
|
21
|
+
def initialize(parser: nil)
|
|
22
|
+
super(parsed: false, parser: parser)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
data/lib/infer_type.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "infer_type/result"
|
|
4
|
+
require_relative "infer_type/parser"
|
|
5
|
+
require_relative "infer_type/registry"
|
|
6
|
+
|
|
7
|
+
module InferType
|
|
8
|
+
class << self
|
|
9
|
+
|
|
10
|
+
# Whether to strip input strings before parsing
|
|
11
|
+
STRIP = true
|
|
12
|
+
|
|
13
|
+
# Primary method to parse a string into a type
|
|
14
|
+
def parse(_input, *allowed_types)
|
|
15
|
+
raise ArgumentError, "Can only parse Strings" unless _input.is_a?(String)
|
|
16
|
+
|
|
17
|
+
# Prepare the string
|
|
18
|
+
str = _input.dup
|
|
19
|
+
str = str.strip if STRIP
|
|
20
|
+
|
|
21
|
+
# Select appropriate parsers
|
|
22
|
+
parsers = select_parsers(allowed_types)
|
|
23
|
+
|
|
24
|
+
# Try each parser
|
|
25
|
+
parsers.each do |parser_class|
|
|
26
|
+
result = parser_class.new.parse(str)
|
|
27
|
+
raise TypeError, "Parser (#{parser_class}) must return a Result object using success(value) or failure() methods" unless result.is_a?(InferType::Result)
|
|
28
|
+
# Return as soon as one succeeds
|
|
29
|
+
return result.value if result.parsed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# All parsers failed: return the original input
|
|
33
|
+
_input
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parser registration methods
|
|
37
|
+
def register(parser_class)
|
|
38
|
+
registry.register(parser_class)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deregister(parser_classes)
|
|
42
|
+
registry.deregister(parser_classes)
|
|
43
|
+
end
|
|
44
|
+
alias unregister deregister
|
|
45
|
+
|
|
46
|
+
def prioritise(*items)
|
|
47
|
+
registry.prioritize(*items)
|
|
48
|
+
end
|
|
49
|
+
alias prioritize prioritise
|
|
50
|
+
|
|
51
|
+
def deprioritise(*items)
|
|
52
|
+
registry.deprioritize(*items)
|
|
53
|
+
end
|
|
54
|
+
alias deprioritize deprioritise
|
|
55
|
+
alias unprioritise deprioritise
|
|
56
|
+
alias unprioritize deprioritise
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def registry
|
|
61
|
+
@registry ||= InferType::Registry.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Select parsers in priority order
|
|
65
|
+
def select_parsers(allowed_types)
|
|
66
|
+
all = registry.parsers
|
|
67
|
+
return all if allowed_types.empty?
|
|
68
|
+
|
|
69
|
+
# Allowed types have been specified
|
|
70
|
+
allowed_types = allowed_types.flatten
|
|
71
|
+
type_priority = {}
|
|
72
|
+
|
|
73
|
+
allowed_types.each_with_index do |type, index|
|
|
74
|
+
parser = registry.parser_for_type(type)
|
|
75
|
+
raise KeyError, "No parser registered for class #{type}" if parser.nil?
|
|
76
|
+
type_priority[type] = index
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
selected = all.select { |parser| type_priority.key?(parser.detects) }
|
|
80
|
+
|
|
81
|
+
# Override registry priority using allowed_types order
|
|
82
|
+
selected.sort_by { |parser| type_priority[parser.detects] }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Load all built-in parsers
|
|
88
|
+
Dir.glob(File.join(__dir__, "infer_type/parsers/*.rb")).each do |file|
|
|
89
|
+
require_relative file
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
InferType.register(InferType::Parsers::IntegerParser)
|
|
93
|
+
InferType.register(InferType::Parsers::FloatParser)
|
|
94
|
+
InferType.register(InferType::Parsers::TrueParser)
|
|
95
|
+
InferType.register(InferType::Parsers::FalseParser)
|
|
96
|
+
InferType.register(InferType::Parsers::NilParser)
|
|
97
|
+
InferType.register(InferType::Parsers::DateParser)
|
|
98
|
+
InferType.register(InferType::Parsers::TimeParser)
|
|
99
|
+
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: infer-type
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Convincible
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.17'
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.17.3
|
|
23
|
+
type: :development
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.17'
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.17.3
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: pry
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.14'
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 0.14.1
|
|
43
|
+
type: :development
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - "~>"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0.14'
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 0.14.1
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: pry-byebug
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.4'
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 3.4.0
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '3.4'
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.4.0
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: rspec
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '3.11'
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.11.0
|
|
83
|
+
type: :development
|
|
84
|
+
prerelease: false
|
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.11'
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: 3.11.0
|
|
93
|
+
description: If a string is a representation of another class, such as "1", converts
|
|
94
|
+
it to that other class. Works for basic Ruby types and can be extended.
|
|
95
|
+
email:
|
|
96
|
+
- development@convincible.media
|
|
97
|
+
executables: []
|
|
98
|
+
extensions: []
|
|
99
|
+
extra_rdoc_files: []
|
|
100
|
+
files:
|
|
101
|
+
- lib/infer_type.rb
|
|
102
|
+
- lib/infer_type/parser.rb
|
|
103
|
+
- lib/infer_type/parsers/date.rb
|
|
104
|
+
- lib/infer_type/parsers/false.rb
|
|
105
|
+
- lib/infer_type/parsers/float.rb
|
|
106
|
+
- lib/infer_type/parsers/integer.rb
|
|
107
|
+
- lib/infer_type/parsers/nil.rb
|
|
108
|
+
- lib/infer_type/parsers/time.rb
|
|
109
|
+
- lib/infer_type/parsers/true.rb
|
|
110
|
+
- lib/infer_type/registry.rb
|
|
111
|
+
- lib/infer_type/result.rb
|
|
112
|
+
homepage: https://github.com/ConvincibleMedia/infer-type
|
|
113
|
+
licenses:
|
|
114
|
+
- MIT
|
|
115
|
+
metadata: {}
|
|
116
|
+
post_install_message:
|
|
117
|
+
rdoc_options: []
|
|
118
|
+
require_paths:
|
|
119
|
+
- lib
|
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 2.1.0
|
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
130
|
+
requirements: []
|
|
131
|
+
rubyforge_project:
|
|
132
|
+
rubygems_version: 2.2.0
|
|
133
|
+
signing_key:
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Convert strings to other classes by inference.
|
|
136
|
+
test_files: []
|