typemap 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/CHANGELOG.md +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +419 -0
- data/lib/typemap/constrain.rb +199 -0
- data/lib/typemap/error.rb +46 -0
- data/lib/typemap/mapper/accessor_shape.rb +74 -0
- data/lib/typemap/mapper/basic_types.rb +24 -0
- data/lib/typemap/mapper/data_shape.rb +88 -0
- data/lib/typemap/mapper/limits.rb +183 -0
- data/lib/typemap/mapper/name_resolver.rb +46 -0
- data/lib/typemap/mapper/plan.rb +174 -0
- data/lib/typemap/mapper/rbs_types.rb +174 -0
- data/lib/typemap/mapper/type.rb +471 -0
- data/lib/typemap/mapper/type_builder.rb +89 -0
- data/lib/typemap/mapper/type_parser.rb +157 -0
- data/lib/typemap/mapper/wording.rb +212 -0
- data/lib/typemap/mapper.rb +298 -0
- data/lib/typemap/version.rb +6 -0
- data/lib/typemap.rb +12 -0
- metadata +67 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typemap
|
|
4
|
+
class Mapper
|
|
5
|
+
# Turns a type syntax tree (see {TypeParser}) into the {Type} the mapper checks, resolving
|
|
6
|
+
# constant names from the declaring namespace. Both type strategies end here, so they accept
|
|
7
|
+
# exactly the same subset; anything outside it raises "unsupported type".
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module TypeBuilder
|
|
11
|
+
# @param node [Array] the syntax tree
|
|
12
|
+
# @param namespace [String, nil] the declaring class's full name, for relative names
|
|
13
|
+
# @return [Object] a {Type}
|
|
14
|
+
# @raise [DefinitionError] on an unsupported type or an unknown constant
|
|
15
|
+
def self.build(node, namespace)
|
|
16
|
+
case node.first
|
|
17
|
+
when :optional then optional(build(node[1], namespace))
|
|
18
|
+
when :union then union(node[1], namespace)
|
|
19
|
+
when :literal then literals([node[1]])
|
|
20
|
+
when :bool then Type::Bool.new
|
|
21
|
+
when :nil then unsupported("nil alone")
|
|
22
|
+
when :untyped then unsupported("untyped; for plain data passed through as the file holds it, write Object")
|
|
23
|
+
when :name then named(node[1], node[2], namespace)
|
|
24
|
+
else raise ArgumentError, "not a type node: #{node.inspect}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param inner [Object] a {Type}
|
|
29
|
+
# @return [Type::Optional]
|
|
30
|
+
def self.optional(inner)
|
|
31
|
+
inner.is_a?(Type::Optional) ? inner : Type::Optional.new(inner: inner)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `"a" | "b"`, optionally with `nil` among them; a union of anything but literals is
|
|
35
|
+
# outside the subset.
|
|
36
|
+
def self.union(members, namespace)
|
|
37
|
+
nilable = members.any? { |member| member.first == :nil }
|
|
38
|
+
rest = members.reject { |member| member.first == :nil }
|
|
39
|
+
unsupported("a union of #{rest.size} types besides nil") if rest.empty?
|
|
40
|
+
built = rest.size == 1 ? build(rest.first, namespace) : literals(rest.map { |member| literal_value(member) })
|
|
41
|
+
nilable ? optional(built) : built
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.literal_value(node)
|
|
45
|
+
unsupported("a union of anything but literals: #{node.inspect}") unless node.first == :literal
|
|
46
|
+
node[1]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.literals(values)
|
|
50
|
+
values.each do |value|
|
|
51
|
+
supported = [String, Integer, TrueClass, FalseClass].any? { |klass| value.is_a?(klass) }
|
|
52
|
+
unsupported("the literal #{value.inspect}: a file cannot hold one") unless supported
|
|
53
|
+
end
|
|
54
|
+
Type::Literals.new(values: values)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.named(name, args, namespace)
|
|
58
|
+
constant = NameResolver.resolve(name, namespace)
|
|
59
|
+
if [Integer, Float, String, Object].include?(constant)
|
|
60
|
+
unsupported("#{name}[…]") unless args.empty?
|
|
61
|
+
constant == Object ? Type::Plain.new : Type::Scalar.new(klass: constant)
|
|
62
|
+
elsif constant == Array
|
|
63
|
+
unsupported("Array without exactly one element type") unless args.size == 1
|
|
64
|
+
Type::ArrayOf.new(element: build(args.first, namespace))
|
|
65
|
+
elsif constant == Hash
|
|
66
|
+
hash_of(args, namespace)
|
|
67
|
+
elsif constant.is_a?(Class) && args.empty?
|
|
68
|
+
Type::ClassRef.new(klass: constant)
|
|
69
|
+
else
|
|
70
|
+
unsupported(args.empty? ? name : "#{name}[…]")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.hash_of(args, namespace)
|
|
75
|
+
unsupported("Hash without a key and a value type") unless args.size == 2
|
|
76
|
+
key = build(args.first, namespace)
|
|
77
|
+
unsupported("Hash[#{key}, …]: a key is always a String") unless key == Type::Scalar.new(klass: String)
|
|
78
|
+
Type::HashOf.new(value: build(args.last, namespace))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private_class_method :union, :literal_value, :literals, :named, :hash_of
|
|
82
|
+
|
|
83
|
+
# @raise [DefinitionError] always
|
|
84
|
+
def self.unsupported(what)
|
|
85
|
+
raise DefinitionError, "unsupported type: #{what}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typemap
|
|
4
|
+
class Mapper
|
|
5
|
+
# Parses a type string in the subset of RBS the mapper supports into a syntax tree for
|
|
6
|
+
# {TypeBuilder}; `BasicTypes` uses it so the `rbs` gem stays optional. The grammar:
|
|
7
|
+
#
|
|
8
|
+
# type := postfix ('|' postfix)*
|
|
9
|
+
# postfix := primary '?'*
|
|
10
|
+
# primary := '(' type ')' | literal | 'bool' | 'nil' | 'untyped' | name ('[' type (',' type)* ']')?
|
|
11
|
+
# name := '::'? Constant ('::' Constant)*
|
|
12
|
+
# literal := "string" | 'string' | integer | true | false
|
|
13
|
+
#
|
|
14
|
+
# A tree node is an Array: `[:name, "Tuile::Size", [args]]`, `[:optional, node]`,
|
|
15
|
+
# `[:union, [nodes]]`, `[:literal, value]`, `[:bool]`, `[:nil]`, `[:untyped]` — the same nodes
|
|
16
|
+
# `RBSTypes` builds from the `rbs` gem's AST, so both strategies share one builder.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class TypeParser
|
|
20
|
+
TOKEN = /\s*(?:(::)|([A-Za-z_]\w*)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(-?\d+(?:\.\d+)?)|([\[\],|?()]))/
|
|
21
|
+
private_constant :TOKEN
|
|
22
|
+
|
|
23
|
+
# @param source [String] the type as written, `"Array[String]?"`
|
|
24
|
+
# @return [Array] the syntax tree
|
|
25
|
+
# @raise [DefinitionError] on a syntax error
|
|
26
|
+
def self.parse(source)
|
|
27
|
+
new(source).parse
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param source [String]
|
|
31
|
+
def initialize(source)
|
|
32
|
+
@source = source
|
|
33
|
+
@tokens = tokenize(source)
|
|
34
|
+
@pos = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Array] the syntax tree
|
|
38
|
+
# @raise [DefinitionError] on a syntax error
|
|
39
|
+
def parse
|
|
40
|
+
node = union
|
|
41
|
+
fail_at("unexpected #{peek.inspect}") if peek
|
|
42
|
+
node
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def tokenize(source)
|
|
48
|
+
tokens = []
|
|
49
|
+
rest = source
|
|
50
|
+
until rest.strip.empty?
|
|
51
|
+
match = TOKEN.match(rest)
|
|
52
|
+
unless match&.begin(0)&.zero?
|
|
53
|
+
raise DefinitionError,
|
|
54
|
+
"type #{source.inspect}: cannot read #{rest.strip.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
tokens << match.captures.compact.first
|
|
58
|
+
rest = match.post_match
|
|
59
|
+
end
|
|
60
|
+
tokens
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def peek = @tokens[@pos]
|
|
64
|
+
|
|
65
|
+
def take
|
|
66
|
+
@tokens[@pos].tap { @pos += 1 }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def expect(token)
|
|
70
|
+
fail_at("expected #{token.inspect}, got #{peek.inspect}") unless peek == token
|
|
71
|
+
take
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fail_at(detail)
|
|
75
|
+
raise DefinitionError, "type #{@source.inspect}: #{detail}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def union
|
|
79
|
+
members = [postfix]
|
|
80
|
+
while peek == "|"
|
|
81
|
+
take
|
|
82
|
+
members << postfix
|
|
83
|
+
end
|
|
84
|
+
members.size == 1 ? members.first : [:union, members]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def postfix
|
|
88
|
+
node = primary
|
|
89
|
+
while peek == "?"
|
|
90
|
+
take
|
|
91
|
+
node = [:optional, node] unless node.first == :optional
|
|
92
|
+
end
|
|
93
|
+
node
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def primary
|
|
97
|
+
token = peek
|
|
98
|
+
fail_at("unexpected end") if token.nil?
|
|
99
|
+
return parenthesized if token == "("
|
|
100
|
+
return [:literal, literal(take)] if token.match?(/\A(?:["'-]|\d)/)
|
|
101
|
+
|
|
102
|
+
case token
|
|
103
|
+
when "bool" then take && [:bool]
|
|
104
|
+
when "nil" then take && [:nil]
|
|
105
|
+
when "untyped" then take && [:untyped]
|
|
106
|
+
when "true" then take && [:literal, true]
|
|
107
|
+
when "false" then take && [:literal, false]
|
|
108
|
+
else name
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parenthesized
|
|
113
|
+
expect("(")
|
|
114
|
+
node = union
|
|
115
|
+
expect(")")
|
|
116
|
+
node
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def literal(token)
|
|
120
|
+
case token
|
|
121
|
+
when /\A"/ then token[1..-2].gsub(/\\(.)/, '\1')
|
|
122
|
+
when /\A'/ then token[1..-2].gsub(/\\(.)/, '\1')
|
|
123
|
+
when /\./ then fail_at("a Float literal is not a type in RBS: #{token}")
|
|
124
|
+
else Integer(token)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def name
|
|
129
|
+
parts = []
|
|
130
|
+
absolute = peek == "::"
|
|
131
|
+
take if absolute
|
|
132
|
+
loop do
|
|
133
|
+
part = take
|
|
134
|
+
fail_at("expected a constant, got #{part.inspect}") unless part&.match?(/\A[A-Z]/)
|
|
135
|
+
parts << part
|
|
136
|
+
break unless peek == "::"
|
|
137
|
+
|
|
138
|
+
take
|
|
139
|
+
end
|
|
140
|
+
[:name, "#{"::" if absolute}#{parts.join("::")}", arguments]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def arguments
|
|
144
|
+
return [] unless peek == "["
|
|
145
|
+
|
|
146
|
+
take
|
|
147
|
+
args = [union]
|
|
148
|
+
while peek == ","
|
|
149
|
+
take
|
|
150
|
+
args << union
|
|
151
|
+
end
|
|
152
|
+
expect("]")
|
|
153
|
+
args
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typemap
|
|
4
|
+
class Mapper
|
|
5
|
+
# How a load error words the found value and the fix, for the source the app names in
|
|
6
|
+
# `load(..., source:)`. The types detect the mistake in the Hash; a wording only spells it:
|
|
7
|
+
#
|
|
8
|
+
# :ruby server: expected Array[Server], got a single Server; wrap it in an array
|
|
9
|
+
# :toml server: expected Array[Server], got a single Server; write [[server]], not [server], or server = [{…}]
|
|
10
|
+
#
|
|
11
|
+
# Each source is a module answering the same six calls: `found(value)`, `wrap(path, entry)`,
|
|
12
|
+
# `unwrap(path)`, `quote(value)`, `unquote` and `misplaced(key, path, owner)`. A hint returns
|
|
13
|
+
# `nil` for none. A dump error is worded for {Ruby}: its reader is the programmer.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module Wording
|
|
17
|
+
# @param source [Symbol] `:ruby`, `:toml`, `:json` or `:yaml`
|
|
18
|
+
# @return [Module] the wording for it
|
|
19
|
+
# @raise [ArgumentError] on any other source
|
|
20
|
+
def self.for(source)
|
|
21
|
+
case source
|
|
22
|
+
when :ruby then Ruby
|
|
23
|
+
when :toml then TOML
|
|
24
|
+
when :json then JSON
|
|
25
|
+
when :yaml then YAML
|
|
26
|
+
else raise ArgumentError, "source: must be :ruby, :toml, :json or :yaml, got #{source.inspect}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param value [Object]
|
|
31
|
+
# @return [String] its `inspect`, cut at 60 characters
|
|
32
|
+
def self.inspected(value)
|
|
33
|
+
text = value.inspect
|
|
34
|
+
text.length > 61 ? "#{text[0, 60]}…" : text
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param key [String]
|
|
38
|
+
# @param owner [String] the enclosing object's path, `""` for the top level
|
|
39
|
+
# @return [String] `sandbox belongs to the top level`
|
|
40
|
+
def self.belongs(key, owner) = "#{key} belongs to #{owner.empty? ? "the top level" : owner}"
|
|
41
|
+
|
|
42
|
+
QUOTE = "quote it to make it a string"
|
|
43
|
+
UNQUOTE = "remove the quotes"
|
|
44
|
+
|
|
45
|
+
# A Hash built in Ruby: the found value's class names the mistake, and there are no quotes
|
|
46
|
+
# to add or remove.
|
|
47
|
+
#
|
|
48
|
+
# value: expected String, got Integer 70
|
|
49
|
+
module Ruby
|
|
50
|
+
# @param value [Object]
|
|
51
|
+
# @return [String] `Integer 70`; bare for `nil`, `true`, `false` and an inspect that names
|
|
52
|
+
# its class already, `#<data Server …>`
|
|
53
|
+
def self.found(value)
|
|
54
|
+
text = Wording.inspected(value)
|
|
55
|
+
[nil, true, false].include?(value) || text.start_with?("#<") ? text : "#{value.class} #{text}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param _path [String]
|
|
59
|
+
# @param _entry [Boolean]
|
|
60
|
+
# @return [String]
|
|
61
|
+
def self.wrap(_path, _entry) = "wrap it in an array"
|
|
62
|
+
|
|
63
|
+
# @param _path [String]
|
|
64
|
+
# @return [String]
|
|
65
|
+
def self.unwrap(_path) = "unwrap it"
|
|
66
|
+
|
|
67
|
+
# @param _value [Object]
|
|
68
|
+
# @return [nil]
|
|
69
|
+
def self.quote(_value) = nil
|
|
70
|
+
|
|
71
|
+
# @return [nil]
|
|
72
|
+
def self.unquote = nil
|
|
73
|
+
|
|
74
|
+
# @param key [String]
|
|
75
|
+
# @param _path [String]
|
|
76
|
+
# @param owner [String]
|
|
77
|
+
# @return [String]
|
|
78
|
+
def self.misplaced(key, _path, owner) = Wording.belongs(key, owner)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# TOML: headers spelled from the path, `llm[0].servers` → `[[llm.servers]]`, since a header
|
|
82
|
+
# names no index.
|
|
83
|
+
#
|
|
84
|
+
# == Implementation details
|
|
85
|
+
#
|
|
86
|
+
# An array hint names the inline spelling too, `server = [{…}]`: Typemap can't tell which
|
|
87
|
+
# one the file used.
|
|
88
|
+
module TOML
|
|
89
|
+
# @param value [Object]
|
|
90
|
+
# @return [String]
|
|
91
|
+
def self.found(value) = Wording.inspected(value)
|
|
92
|
+
|
|
93
|
+
# @param path [String]
|
|
94
|
+
# @param entry [Boolean] whether the lone value is an object
|
|
95
|
+
# @return [String]
|
|
96
|
+
def self.wrap(path, entry)
|
|
97
|
+
return "wrap it in [ ]" unless entry && keyed?(path)
|
|
98
|
+
|
|
99
|
+
"write #{header(path, array: true)}, not #{header(path)}, or #{key(path)} = [{…}]"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param path [String]
|
|
103
|
+
# @return [String]
|
|
104
|
+
def self.unwrap(path)
|
|
105
|
+
return "remove the [ ]" unless keyed?(path)
|
|
106
|
+
|
|
107
|
+
"write #{header(path)}, not #{header(path, array: true)}, or #{key(path)} = {…}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @param _value [Object]
|
|
111
|
+
# @return [String]
|
|
112
|
+
def self.quote(_value) = QUOTE
|
|
113
|
+
|
|
114
|
+
# @return [String]
|
|
115
|
+
def self.unquote = UNQUOTE
|
|
116
|
+
|
|
117
|
+
# @param key [String]
|
|
118
|
+
# @param path [String] the object the key sits in
|
|
119
|
+
# @param owner [String] the enclosing object that has the key, `""` for the top level
|
|
120
|
+
# @return [String] `sandbox belongs to code, under [code] before [code.panel]`
|
|
121
|
+
def self.misplaced(key, path, owner)
|
|
122
|
+
where = owner.empty? ? "before any [header]" : "under #{header(owner)} before #{header(path)}"
|
|
123
|
+
"#{Wording.belongs(key, owner)}, #{where}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param path [String] `llm[0].params`
|
|
127
|
+
# @param array [Boolean] whether to spell it as an array entry's header
|
|
128
|
+
# @return [String] `[llm.params]`; `[[llm]]` for `llm[0]`
|
|
129
|
+
def self.header(path, array: path.end_with?("]"))
|
|
130
|
+
name = path.split(".").map { |key| bare(key.sub(/\[\d+\]\z/, "")) }.join(".")
|
|
131
|
+
array ? "[[#{name}]]" : "[#{name}]"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param path [String]
|
|
135
|
+
# @return [Boolean] whether the value sits under a key, which a header can name — not at the
|
|
136
|
+
# root, nor directly in an array
|
|
137
|
+
def self.keyed?(path) = !path.empty? && !path.end_with?("]")
|
|
138
|
+
|
|
139
|
+
# @param path [String] `llm[0].servers`
|
|
140
|
+
# @return [String] `servers`, as TOML writes it
|
|
141
|
+
def self.key(path) = bare(path.split(".").last)
|
|
142
|
+
|
|
143
|
+
# @param key [String]
|
|
144
|
+
# @return [String] the key as TOML writes it, quoted unless bare
|
|
145
|
+
def self.bare(key) = key.match?(/\A[A-Za-z0-9_-]+\z/) ? key : key.inspect
|
|
146
|
+
private_class_method :header, :keyed?, :key, :bare
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# JSON: brackets and braces.
|
|
150
|
+
module JSON
|
|
151
|
+
# @param value [Object]
|
|
152
|
+
# @return [String]
|
|
153
|
+
def self.found(value) = Wording.inspected(value)
|
|
154
|
+
|
|
155
|
+
# @param _path [String]
|
|
156
|
+
# @param _entry [Boolean]
|
|
157
|
+
# @return [String]
|
|
158
|
+
def self.wrap(_path, _entry) = "wrap it in [ ]"
|
|
159
|
+
|
|
160
|
+
# @param _path [String]
|
|
161
|
+
# @return [String]
|
|
162
|
+
def self.unwrap(_path) = "remove the [ ]"
|
|
163
|
+
|
|
164
|
+
# @param _value [Object]
|
|
165
|
+
# @return [String]
|
|
166
|
+
def self.quote(_value) = QUOTE
|
|
167
|
+
|
|
168
|
+
# @return [String]
|
|
169
|
+
def self.unquote = UNQUOTE
|
|
170
|
+
|
|
171
|
+
# @param key [String]
|
|
172
|
+
# @param path [String] the object the key sits in
|
|
173
|
+
# @param owner [String] the enclosing object that has the key, `""` for the top level
|
|
174
|
+
# @return [String] `sandbox belongs to the top level; move it out of pane's { }`
|
|
175
|
+
def self.misplaced(key, path, owner) = "#{Wording.belongs(key, owner)}; move it out of #{path}'s { }"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# YAML in block style: `- ` and indentation.
|
|
179
|
+
module YAML
|
|
180
|
+
# @param value [Object]
|
|
181
|
+
# @return [String]
|
|
182
|
+
def self.found(value) = Wording.inspected(value)
|
|
183
|
+
|
|
184
|
+
# @param _path [String]
|
|
185
|
+
# @param _entry [Boolean]
|
|
186
|
+
# @return [String]
|
|
187
|
+
def self.wrap(_path, _entry) = 'put "- " before it'
|
|
188
|
+
|
|
189
|
+
# @param _path [String]
|
|
190
|
+
# @return [String]
|
|
191
|
+
def self.unwrap(_path) = 'remove the "- "'
|
|
192
|
+
|
|
193
|
+
# Psych reads YAML 1.1, so `no` loads as `false`: the Norway problem.
|
|
194
|
+
#
|
|
195
|
+
# @param value [Object]
|
|
196
|
+
# @return [String]
|
|
197
|
+
def self.quote(value)
|
|
198
|
+
[true, false].include?(value) ? "#{QUOTE}; YAML reads yes, no, on and off as booleans" : QUOTE
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [String]
|
|
202
|
+
def self.unquote = UNQUOTE
|
|
203
|
+
|
|
204
|
+
# @param key [String]
|
|
205
|
+
# @param _path [String]
|
|
206
|
+
# @param owner [String]
|
|
207
|
+
# @return [String] `sandbox belongs to the top level; dedent it`
|
|
208
|
+
def self.misplaced(key, _path, owner) = "#{Wording.belongs(key, owner)}; dedent it"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|