soml-lang 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.
data/lib/soml/tree.rb ADDED
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOML
4
+ # The syntax tree of a valid document, for {SOML.format} and {SOML.edit}.
5
+ #
6
+ # It is a port of the JS reference's `tree.ts`, with only what the formatter and the editor read. Every offset is a byte offset into the UTF-8 source, and `finish` is the offset after the node. A node has no line and column, a scalar has no value, and there is no list of tokens. The document is checked by {Parser} first, so the builder can assume valid input, and the comments are collected in source order.
7
+ #
8
+ # @api private
9
+ class Tree
10
+ # The nodes are structs rather than `Data`, whose `new` builds a hash of keywords for every node, which made building a tree several times as slow as parsing the document. Nothing changes a node once it is made.
11
+
12
+ # A `# ...` comment (`:line`) or a `/* ... */` comment (`:block`).
13
+ Comment = Struct.new(:type, :start, :finish)
14
+
15
+ # An object, written with braces or, only at the top level, without them.
16
+ ObjectNode = Struct.new(:members, :braced, :start, :finish) do
17
+ def type = :object
18
+ end
19
+
20
+ ArrayNode = Struct.new(:elements, :start, :finish) do
21
+ def type = :array
22
+ end
23
+
24
+ # A `key: value` member.
25
+ MemberNode = Struct.new(:key, :value, :start, :finish) do
26
+ def type = :member
27
+ end
28
+
29
+ # A key, with its decoded `value`.
30
+ KeyNode = Struct.new(:value, :start, :finish)
31
+
32
+ # A value that is not a collection. `block` is whether it is a block string.
33
+ ScalarNode = Struct.new(:block, :start, :finish) do
34
+ def type = :scalar
35
+ end
36
+
37
+ # The characters that end the search for the closing quote of a `"..."` string.
38
+ ESCAPED_STRING_SPECIAL = /["\\]/
39
+
40
+ private_constant(*constants(false))
41
+
42
+ # @return [String] The source, in UTF-8.
43
+ attr_reader :source
44
+
45
+ # @return [ObjectNode, ArrayNode] The document's collection.
46
+ attr_reader :body
47
+
48
+ # @return [Array<Comment>] Every comment, in source order.
49
+ attr_reader :comments
50
+
51
+ # @param source [String] The document, decoded as {SOML.parse} decodes it.
52
+ # @raise [ParseError] When the document is not valid, the same as {SOML.parse}.
53
+ def initialize(source)
54
+ # Validates the whole document, so the builder below can assume valid input.
55
+ parser = Parser.new(source)
56
+ parser.parse
57
+ @source = parser.source
58
+ @length = @source.bytesize
59
+ @index = 0
60
+ @comments = []
61
+
62
+ skip_trivia
63
+
64
+ @body = case @source.getbyte(@index)
65
+ when 0x7B # {
66
+ parse_object
67
+ when 0x5B # [
68
+ parse_array
69
+ else
70
+ parse_bare_object
71
+ end
72
+
73
+ skip_trivia
74
+ end
75
+
76
+ private
77
+
78
+ def skip_trivia
79
+ source = @source
80
+ index = @index
81
+
82
+ while (byte = source.getbyte(index))
83
+ case byte
84
+ when 0x20, 0x09, 0x0A
85
+ index += 1
86
+ when 0x23 # #
87
+ finish = source.byteindex("\n", index) || @length
88
+ @comments << Comment.new(:line, index, finish)
89
+ index = finish
90
+ when 0x2F # /
91
+ # The parser checked that every block comment ends, so the search finds its `*/`.
92
+ finish = source.byteindex('*/', index + 2) + 2
93
+ @comments << Comment.new(:block, index, finish)
94
+ index = finish
95
+ else
96
+ break
97
+ end
98
+ end
99
+
100
+ @index = index
101
+ end
102
+
103
+ def parse_bare_object
104
+ members = []
105
+
106
+ loop do
107
+ members << parse_member
108
+ skip_trivia
109
+ break if @index >= @length
110
+ end
111
+
112
+ ObjectNode.new(members, false, members.first.start, members.last.finish)
113
+ end
114
+
115
+ def parse_object
116
+ start = @index
117
+ members = parse_items(0x7D) { parse_member }
118
+ ObjectNode.new(members, true, start, @index)
119
+ end
120
+
121
+ def parse_array
122
+ start = @index
123
+ elements = parse_items(0x5D) { parse_value }
124
+ ArrayNode.new(elements, start, @index)
125
+ end
126
+
127
+ # The items between an opening bracket and its `closing` bracket, which is consumed too. The parser has already checked the separators, including that a comma is on the line of the item before it.
128
+ def parse_items(closing)
129
+ items = []
130
+ @index += 1
131
+ skip_trivia
132
+
133
+ until @source.getbyte(@index) == closing
134
+ items << yield
135
+ skip_trivia
136
+ next unless @source.getbyte(@index) == 0x2C # ,
137
+
138
+ @index += 1
139
+ skip_trivia
140
+ end
141
+
142
+ @index += 1
143
+ items
144
+ end
145
+
146
+ def parse_member
147
+ key = parse_key
148
+ @index += 1 # :
149
+ skip_trivia
150
+ value = parse_value
151
+ MemberNode.new(key, value, key.start, value.finish)
152
+ end
153
+
154
+ def parse_key
155
+ source = @source
156
+ start = @index
157
+ byte = source.getbyte(start)
158
+
159
+ if byte == 0x27 || byte == 0x22 # ' "
160
+ finish = string_end(start)
161
+ @index = finish
162
+ text = source.byteslice(start + 1, finish - start - 2)
163
+
164
+ # A `"..."` key with an escape is decoded by the parser itself, so that the tree and `SOML.parse` cannot disagree.
165
+ return KeyNode.new(byte == 0x22 && text.include?('\\') ? read_scalar(start, finish) : text, start, finish)
166
+ end
167
+
168
+ finish = start
169
+ finish += 1 while (byte = source.getbyte(finish)) && BARE_KEY_BYTE[byte]
170
+ @index = finish
171
+ KeyNode.new(source.byteslice(start, finish - start), start, finish)
172
+ end
173
+
174
+ def parse_value
175
+ source = @source
176
+ start = @index
177
+
178
+ case (byte = source.getbyte(start))
179
+ when 0x7B # {
180
+ parse_object
181
+ when 0x5B # [
182
+ parse_array
183
+ when 0x27, 0x22 # ' "
184
+ return parse_block_string(byte) if source.getbyte(start + 1) == byte && source.getbyte(start + 2) == byte
185
+
186
+ @index = string_end(start)
187
+ ScalarNode.new(false, start, @index)
188
+ else
189
+ finish = start + 1
190
+ finish += 1 while (byte = source.getbyte(finish)) && NUMBER_BYTE[byte]
191
+ @index = finish
192
+ ScalarNode.new(false, start, finish)
193
+ end
194
+ end
195
+
196
+ # The offset after the closing quote of the one-line string at `start`. `'...'` ends at the next `'`. `"..."` ends at the next `"` that is not escaped, and `\` always starts a two-character escape or a `\u{…}`, which holds no quote.
197
+ def string_end(start)
198
+ source = @source
199
+ return source.byteindex("'", start + 1) + 1 if source.getbyte(start) == 0x27
200
+
201
+ index = start + 1
202
+
203
+ loop do
204
+ index = source.byteindex(ESCAPED_STRING_SPECIAL, index)
205
+ return index + 1 if source.getbyte(index) == 0x22
206
+
207
+ index += 2
208
+ end
209
+ end
210
+
211
+ # The block ends at the first line whose first non-whitespace content is a run of exactly as many quotes as the opening delimiter.
212
+ def parse_block_string(quote)
213
+ source = @source
214
+ start = @index
215
+ delimiter_length = 0
216
+ delimiter_length += 1 while source.getbyte(start + delimiter_length) == quote
217
+ line_start = source.byteindex("\n", start) + 1
218
+
219
+ loop do
220
+ delimiter_start = line_start
221
+ delimiter_start += 1 while (byte = source.getbyte(delimiter_start)) == 0x20 || byte == 0x09
222
+ run_length = 0
223
+ run_length += 1 while source.getbyte(delimiter_start + run_length) == quote
224
+
225
+ if run_length == delimiter_length
226
+ @index = delimiter_start + delimiter_length
227
+ return ScalarNode.new(true, start, @index)
228
+ end
229
+
230
+ line_start = source.byteindex("\n", line_start) + 1
231
+ end
232
+ end
233
+
234
+ # The value of the scalar from `start` to `finish`, read by the parser. A scalar's validity does not depend on its context, so it parses on its own inside an array.
235
+ def read_scalar(start, finish)
236
+ Parser.new("[#{@source.byteslice(start, finish - start)}]").parse.first
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOML
4
+ # The version of this gem.
5
+ VERSION = '0.0.1'
6
+ end
data/lib/soml-lang.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The gem is named `soml-lang`, so that `Bundler.require` finds this file. The library is `require 'soml'`.
4
+ require_relative 'soml'
data/lib/soml.rb ADDED
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'soml/version'
4
+
5
+ # Read and write SOML, a config format for humans.
6
+ #
7
+ # @see https://github.com/soml-lang/soml The specification
8
+ module SOML
9
+ # The deepest nesting either direction accepts. Every array and object is one level, including the document's own collection.
10
+ MAX_DEPTH = 100
11
+
12
+ INT64_MIN = -(2**63)
13
+ INT64_MAX = (2**63) - 1
14
+
15
+ # The duration units in their required order, with their length in nanoseconds.
16
+ DURATION_UNITS = {
17
+ 'h' => 3_600_000_000_000,
18
+ 'm' => 60_000_000_000,
19
+ 's' => 1_000_000_000,
20
+ 'ms' => 1_000_000,
21
+ 'us' => 1000,
22
+ 'ns' => 1
23
+ }.freeze
24
+
25
+ # A byte that a bare key can hold, as a table of 256 booleans, for the {Parser} and the {Tree}.
26
+ BARE_KEY_BYTE = Array.new(256) { |byte| byte.chr.match?(/[A-Za-z0-9_-]/) }.freeze
27
+
28
+ # A number, an instant, or a duration is one maximal run of these bytes, which the {Parser} then validates as a whole. A keyword ends at the first byte that is not one, too, because the parser checked that it is followed by whitespace, a separator, a closing bracket, a comment, or the end.
29
+ NUMBER_BYTE = Array.new(256) { |byte| byte.chr.match?(/[A-Za-z0-9_.:+-]/) }.freeze
30
+
31
+ # A line of a block string without one of these is blank. A search for one character, because a repeat over a long line needs memory for every character.
32
+ NOT_BLANK_CHARACTER = /[^\t ]/
33
+
34
+ # The default of the `value` of {SOML.edit}, which tells a missing value from `nil`.
35
+ NO_VALUE = Object.new.freeze
36
+
37
+ # The indentation of each level, so that the {Generator} and the {Printer} make no string for each line they indent. The {Printer} indents a value after a comment one level deeper than its key, so each container can be two levels deeper than the one that holds it, and a value after a comment in the deepest container is at level 200.
38
+ INDENTS = Array.new((MAX_DEPTH * 2) + 1) { |level| ("\t" * level).freeze }.freeze
39
+
40
+ private_constant :MAX_DEPTH, :INT64_MIN, :INT64_MAX, :DURATION_UNITS, :NO_VALUE, :BARE_KEY_BYTE, :NUMBER_BYTE, :NOT_BLANK_CHARACTER, :INDENTS
41
+
42
+ # Whether a key can be written without quotes, as a bare key: one or more ASCII letters, digits, `_`, or `-`, in any order, so `404` and `-x` are bare keys too. {SOML.generate} writes such a key bare and quotes every other key.
43
+ #
44
+ # @param key [String, Symbol] A key in UTF-8, or a symbol, which is read as its name. A key in an encoding that is not compatible with ASCII, such as UTF-16, is read as the UTF-8 string it converts to, as {SOML.generate} reads it. Any other object is not a key, so it gives false.
45
+ # @return [Boolean]
46
+ #
47
+ # @example
48
+ # SOML.bare_key?('content-type')
49
+ # #=> true
50
+ #
51
+ # SOML.bare_key?('a.b')
52
+ # #=> false
53
+ def self.bare_key?(key)
54
+ # Only a String or a Symbol is a key. Anything else, such as a number or nil, could never be a bare key, so the answer is false rather than a crash on its missing `encoding`.
55
+ return false unless String === key || Symbol === key
56
+
57
+ # The byte scan needs a String, so a Symbol is read as its name. A String, even a subclass, is left as it is, as the regex did.
58
+ key = key.to_s unless String === key
59
+ key = key.encode(Encoding::UTF_8) unless key.encoding.ascii_compatible?
60
+
61
+ # A scan of the bytes, which is faster than the regex it replaces and decides the same: a byte that is not an ASCII letter, digit, `_`, or `-` means the key needs quotes. A non-ASCII character is a run of bytes none of which a bare key can hold.
62
+ return false if key.empty?
63
+
64
+ index = 0
65
+ length = key.bytesize
66
+
67
+ while index < length
68
+ return false unless BARE_KEY_BYTE[key.getbyte(index)]
69
+
70
+ index += 1
71
+ end
72
+
73
+ true
74
+ rescue ArgumentError, EncodingError
75
+ # Invalid bytes, or a key that does not convert to UTF-8.
76
+ false
77
+ end
78
+
79
+ # Parse a document.
80
+ #
81
+ # @param source [String] The document. A UTF-8, binary, or US-ASCII string is read as UTF-8, and a string in another encoding is converted to UTF-8 first.
82
+ # @param symbolize_names [Boolean] Whether object keys are symbols rather than strings.
83
+ # @param freeze [Boolean] Whether the result is deeply frozen, which also makes it shareable between ractors.
84
+ # @return [Hash, Array] The object or array the document contains.
85
+ # @raise [ParseError] When the document is not valid.
86
+ # @raise [TypeError] When `source` is not a string.
87
+ # @raise [EncodingError] When `source` is in an encoding other than UTF-8, binary, or US-ASCII, and does not convert to UTF-8.
88
+ #
89
+ # @example
90
+ # SOML.parse(<<~SOML)
91
+ # name: 'api-gateway'
92
+ # replicas: 3
93
+ # timeout: 30s
94
+ # postgres: {host: 'db.internal'}
95
+ # SOML
96
+ # #=> {"name" => "api-gateway", "replicas" => 3, "timeout" => #<SOML::Duration 30s>, "postgres" => {"host" => "db.internal"}}
97
+ def self.parse(source, symbolize_names: false, freeze: false)
98
+ Parser.new(source, symbolize_names:, freeze:).parse
99
+ end
100
+
101
+ # Read and parse a file.
102
+ #
103
+ # The file is read in binary mode, so that a carriage return, which a document may not contain, is not hidden by a text-mode conversion. An error names the file, as in `Duplicate key port at config.soml:3:1`.
104
+ #
105
+ # @param path [String, #to_path] The file, such as a `Pathname` or a `File`.
106
+ # @param symbolize_names [Boolean] See {SOML.parse}.
107
+ # @param freeze [Boolean] See {SOML.parse}.
108
+ # @return [Hash, Array] The object or array the document contains.
109
+ # @raise [ParseError] When the document is not valid. Its message and {ParseError#path} name the file.
110
+ # @raise [SystemCallError] When the file cannot be read, such as `Errno::ENOENT`.
111
+ #
112
+ # @example
113
+ # config = SOML.load_file('config.soml', symbolize_names: true)
114
+ def self.load_file(path, symbolize_names: false, freeze: false)
115
+ Parser.new(File.binread(path), symbolize_names:, freeze:, path: File.path(path)).parse
116
+ end
117
+
118
+ # Write a hash or an array as SOML.
119
+ #
120
+ # Members keep the order of their hash, nesting is written with braces and tabs, and the text ends with one line feed. Comments and block strings are never written. Everything but the order of members is canonical form, so the text is canonical form when the keys are already sorted.
121
+ #
122
+ # With `canonical: true`, members are sorted by key too, so the text is canonical form. Two equal values then give the same text, which can be hashed, signed, or compared.
123
+ #
124
+ # @param value [Hash, Array] The document.
125
+ # @param canonical [Boolean] Whether to write canonical form, with members sorted by key.
126
+ # @return [String] The text, in UTF-8.
127
+ # @raise [GenerateError] For a value that cannot be written, such as NaN, an integer outside the 64-bit range, a key that is not a string or a symbol, or an object of another class.
128
+ #
129
+ # @example
130
+ # SOML.generate({name: 'api-gateway', replicas: 3, ratio: 0.5})
131
+ # #=> "name: 'api-gateway'\nreplicas: 3\nratio: 0.5\n"
132
+ #
133
+ # SOML.generate({name: 'api-gateway', replicas: 3, ratio: 0.5}, canonical: true)
134
+ # #=> "name: 'api-gateway'\nratio: 0.5\nreplicas: 3\n"
135
+ def self.generate(value, canonical: false)
136
+ Generator.new(canonical:).generate(value)
137
+ end
138
+
139
+ # Format a document. Returns the document with its layout normalized, ending with one line feed.
140
+ #
141
+ # The layout follows the formatter in the specification: one tab per level, every member and item on its own line with no commas, and no trailing whitespace or runs of blank lines. An object or an array whose brackets are on one line stays on one line, as in `ports: [80, 443]`, with a comma and a space between its members or items. To give a one-line container one member or item per line, put a line break anywhere inside it. Comments, member order, block strings, and the spelling of every value stay as they are, so the value never changes. The one change inside a block comment is the same layout rule: trailing whitespace is removed, and runs of blank lines collapse to one.
142
+ #
143
+ # In detail, as the specification states:
144
+ #
145
+ # - A block comment that a value follows on the same line, as in `1, /* note */ 2`, stays in front of that value when each item goes on its own line.
146
+ # - One space follows each `:`, as in canonical form.
147
+ # - A value on the line after its `key:` moves up to that line, unless a comment comes between them. Then it stays on its own line, one level deeper than the key, and the blank lines between them are removed.
148
+ # - A block string begins on the line after its key, and its delimiters and content have the indentation of that line. Its lines are not otherwise changed.
149
+ # - There are no blank lines at the start of the file or directly inside brackets.
150
+ #
151
+ # @param source [String] The document, read as {SOML.parse} reads it.
152
+ # @return [String] The formatted document, in UTF-8, ending with one line feed.
153
+ # @raise [ParseError] When the document is not valid, the same as {SOML.parse}.
154
+ # @raise [TypeError] When `source` is not a string.
155
+ # @raise [EncodingError] When `source` is in an encoding other than UTF-8, binary, or US-ASCII, and does not convert to UTF-8.
156
+ #
157
+ # @example
158
+ # SOML.format('pool: {min: 2, max: 16,} # Connections')
159
+ # #=> "pool: {min: 2, max: 16} # Connections\n"
160
+ #
161
+ # SOML.format("pool: {\nmin: 2, max: 16}")
162
+ # #=> "pool: {\n\tmin: 2\n\tmax: 16\n}\n"
163
+ def self.format(source)
164
+ Printer.new(Tree.new(source)).print
165
+ end
166
+
167
+ # Change one value in a document, and keep everything else as it is written: comments, member order, layout, and the spelling of every other value. For tools that update a config file, such as a dependency bumper or a `set` command.
168
+ #
169
+ # The value at `path` is replaced, or added when it does not exist yet, and `delete: true` removes it. Only the changed part of the text is rewritten, and an edit to a formatted document leaves it formatted.
170
+ #
171
+ # - A new value is written as {SOML.generate} writes it, in canonical form except that its members keep their order, at the indentation of its line. So `0xFF` that is replaced by `255` is written `255`. In a container that is on one line, it is written on one line too, so `[1, 2]` with a new item `{a: 3}` becomes `[1, 2, {a: 3}]`.
172
+ # - A new member goes after the last member of its object. Missing objects on the way are created.
173
+ # - A new item can be added at the end of an array, with the index that is its length.
174
+ # - A new member or item goes on its own line without a comma, after the comments that the one before it owns (see below), and the commas of the other members and items stay as they are. When something follows the member or item before it on the same line, such as the closing bracket of the one-line container `{a: 1}`, it goes on that line after a comma, as in `{a: 1, b: 2}`. In an empty `[]` or `{}`, it goes on a line of its own, unless that container is inside a container on one line, so `a: [1, []]` becomes `a: [1, [2]]`.
175
+ # - A removed member or item takes the comments it owns, which in a formatted document are the ones that {SOML.format} keeps with it: the comments after it on its line, also after its comma when nothing else follows there, and the block comments before it on its line, after the comma or bracket before it. So removing `2` from `[1, /* note */ 2]` gives `[1]`. A comment on a line of its own belongs to no member or item, so it stays. A removed member or item also removes its lines when nothing else is on them. A line that a block comment after it continues onto counts as one of its lines. Removing every item of a container closes it up to `[]` or `{}`, unless a comment is left inside.
176
+ # - A removed member or item takes its comma with it. When the removed items are the last ones and have no comma after them, they take the comma directly before them instead, when only spaces, tabs, and the comments they own are between, so `[1, 2]` becomes `[1]`.
177
+ # - Removing the only member of a document without braces leaves `{}`.
178
+ #
179
+ # Comments inside a value that is replaced or removed are removed with it. In a layout that {SOML.format} never writes, such as an item after a closing block string delimiter on its line, a new item can be indented differently from its neighbors, but the document is always valid and has the right value. Removing a value that does not exist changes nothing and is not an error: a missing member, a missing object or array on the way, or an index at or past the end of its array. So removing the same path twice is safe. The result has the same bytes as `source` exactly when nothing was removed, so `SOML.edit(source, path, delete: true).b == source.b` tells whether something was removed. The `.b` compares the bytes, because a source read in binary mode is not `==` to the UTF-8 result when it has a character that is not ASCII. This holds for a source in UTF-8, binary, or US-ASCII, because one in another encoding is converted to UTF-8.
180
+ #
181
+ # The result is parsed before it is returned, so a bug in `edit` raises a `RuntimeError` rather than returning a broken document.
182
+ #
183
+ # @overload edit(source, path, value)
184
+ # @param source [String] The document, read as {SOML.parse} reads it.
185
+ # @param path [Array<String, Symbol, Integer>] The keys and array indexes that lead to the value, such as `['servers', 0, 'port']`.
186
+ # @param value [Object] The new value, of the types that {SOML.generate} accepts. Write a `Hash` with braces, because without them Ruby passes its members as keyword arguments.
187
+ # @return [String] The changed document, in UTF-8.
188
+ # @overload edit(source, path, delete: true)
189
+ # @param source [String] The document, read as {SOML.parse} reads it.
190
+ # @param path [Array<String, Symbol, Integer>] The keys and array indexes that lead to the value.
191
+ # @param delete [true] Remove the value.
192
+ # @return [String] The changed document, in UTF-8.
193
+ # @raise [ParseError] When the document is not valid, the same as {SOML.parse}.
194
+ # @raise [ArgumentError] When both a value and `delete: true` are given, or neither.
195
+ # @raise [TypeError] When `source` is not a string, when `path` is not a non-empty array of keys and array indexes, when it leads through a value that is not an object or an array, or when it has a key where an array is or an index where an object is.
196
+ # @raise [IndexError] When a value is set at an index past the end of its array, or at an index under a value that does not exist.
197
+ # @raise [GenerateError] For a value or a key that {SOML.generate} cannot write, and when the change would nest the document more than 100 levels deep.
198
+ # @raise [EncodingError] When `source` is in an encoding other than UTF-8, binary, or US-ASCII, and does not convert to UTF-8.
199
+ #
200
+ # @example
201
+ # SOML.edit("name: 'api' # The service\nport: 8080\n", ['port'], 9090)
202
+ # #=> "name: 'api' # The service\nport: 9090\n"
203
+ #
204
+ # SOML.edit("postgres: {\n\thost: 'db'\n}\n", ['postgres', 'port'], 5432)
205
+ # #=> "postgres: {\n\thost: 'db'\n\tport: 5432\n}\n"
206
+ #
207
+ # SOML.edit("a: 1\nb: 2\n", ['a'], delete: true)
208
+ # #=> "b: 2\n"
209
+ def self.edit(source, path, value = NO_VALUE, delete: false)
210
+ if delete
211
+ raise ArgumentError, 'Pass either a value or delete: true, not both' unless NO_VALUE.equal?(value)
212
+ elsif NO_VALUE.equal?(value)
213
+ raise ArgumentError, 'Pass the new value, or delete: true to remove the value'
214
+ end
215
+
216
+ Editor.new(source, path, value, delete:).edit
217
+ end
218
+ end
219
+
220
+ require_relative 'soml/error'
221
+ require_relative 'soml/duration'
222
+ require_relative 'soml/parser'
223
+ require_relative 'soml/generator'
224
+ require_relative 'soml/tree'
225
+ require_relative 'soml/format'
226
+ require_relative 'soml/edit'
data/license ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.