enolib 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +52 -0
  4. data/lib/enolib.rb +42 -0
  5. data/lib/enolib/constants.rb +16 -0
  6. data/lib/enolib/context.rb +220 -0
  7. data/lib/enolib/elements/element.rb +42 -0
  8. data/lib/enolib/elements/element_base.rb +141 -0
  9. data/lib/enolib/elements/empty.rb +9 -0
  10. data/lib/enolib/elements/field.rb +63 -0
  11. data/lib/enolib/elements/fieldset.rb +151 -0
  12. data/lib/enolib/elements/fieldset_entry.rb +15 -0
  13. data/lib/enolib/elements/list.rb +107 -0
  14. data/lib/enolib/elements/list_item.rb +13 -0
  15. data/lib/enolib/elements/missing/missing_element_base.rb +44 -0
  16. data/lib/enolib/elements/missing/missing_empty.rb +13 -0
  17. data/lib/enolib/elements/missing/missing_field.rb +13 -0
  18. data/lib/enolib/elements/missing/missing_fieldset.rb +29 -0
  19. data/lib/enolib/elements/missing/missing_fieldset_entry.rb +13 -0
  20. data/lib/enolib/elements/missing/missing_list.rb +33 -0
  21. data/lib/enolib/elements/missing/missing_section.rb +105 -0
  22. data/lib/enolib/elements/missing/missing_section_element.rb +53 -0
  23. data/lib/enolib/elements/missing/missing_value_element_base.rb +21 -0
  24. data/lib/enolib/elements/section.rb +560 -0
  25. data/lib/enolib/elements/section_element.rb +141 -0
  26. data/lib/enolib/elements/value_element_base.rb +79 -0
  27. data/lib/enolib/errors.rb +25 -0
  28. data/lib/enolib/errors/parsing.rb +136 -0
  29. data/lib/enolib/errors/selections.rb +83 -0
  30. data/lib/enolib/errors/validation.rb +146 -0
  31. data/lib/enolib/grammar_regexp.rb +103 -0
  32. data/lib/enolib/lookup.rb +235 -0
  33. data/lib/enolib/messages/de.rb +79 -0
  34. data/lib/enolib/messages/en.rb +79 -0
  35. data/lib/enolib/messages/es.rb +79 -0
  36. data/lib/enolib/parse.rb +9 -0
  37. data/lib/enolib/parser.rb +708 -0
  38. data/lib/enolib/register.rb +24 -0
  39. data/lib/enolib/reporters/html_reporter.rb +115 -0
  40. data/lib/enolib/reporters/reporter.rb +258 -0
  41. data/lib/enolib/reporters/terminal_reporter.rb +107 -0
  42. data/lib/enolib/reporters/text_reporter.rb +46 -0
  43. metadata +130 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8aeeb10368acccea0f1381c34c6101a552493aea19ef50d8bdc7616f9a898cc7
4
+ data.tar.gz: 576efc4e4191c945a4d262789e2d353d3521d712b63d1ce4236937d7a556e387
5
+ SHA512:
6
+ metadata.gz: a37196b8d2b69595e22f68b2b9025f64ec01028442ceddf4d6181cea71903f570561d0c2487d515bbad98ecde76c912284be282f8b3c54590cd026c716c8da33
7
+ data.tar.gz: e9c8143539d6eb1fbf7a6e594208a70d5d6cad25f77d471556b1091d7bd4b3cfd268dbb28a0b764cbc0701b01184bd428cedfd6aecea24bac9d9c222ec8873f6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Simon Repp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # enolib
2
+
3
+ The cross-language eno standard library
4
+
5
+ ## Installation
6
+
7
+ ### Bundler
8
+
9
+ Add `enolib` to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'enolib'
13
+ ```
14
+ Then let bundler install it for you:
15
+
16
+ ```
17
+ bundle
18
+ ```
19
+
20
+ ### Manually
21
+
22
+ Alternatively you can also install it manually:
23
+
24
+ ```
25
+ gem install enolib
26
+ ```
27
+
28
+ ## Getting started
29
+
30
+ A minimal example to read an eno document directly from a string with `enolib`:
31
+
32
+ ```ruby
33
+ require 'enolib'
34
+
35
+ document = Enolib.parse('Greeting: Hello World!')
36
+
37
+ puts document.field('Greeting').required_string_value #=> 'Hello World!'
38
+ ```
39
+
40
+ ## Documentation
41
+
42
+ Until available please use the javascript or python documentation at [eno-lang.org/enolib](https://eno-lang.org/enolib/) and replace all shown syntax with the ruby equivalent (ie. different case for the most part) - except for language specifics the API is exactly the same for all implementations.
43
+
44
+ ## Beta notice
45
+
46
+ enolib is currently in beta, if you encounter any issues please report them in the issue tracker - thank you for your help!
47
+
48
+ ## Compatibility notice
49
+
50
+ enolib supports the not yet completely ratified [final specification](https://github.com/eno-lang/eno/tree/master/rfcs-final-spec) for eno. You are encouraged to use it in your projects already, thereby helping us to gather insights on whether there are any remaining flaws in the proposed final specifications (you're very welcome to report such issues if you encounter them).
51
+
52
+ From a practical point of view there are not likely any major changes ahead. Unless you build your application around edge cases of the exotic language features you should be perfectly safe and future-proof.
data/lib/enolib.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'enolib/constants'
4
+ require 'enolib/context'
5
+ require 'enolib/errors'
6
+ require 'enolib/grammar_regexp'
7
+ require 'enolib/messages/en'
8
+ require 'enolib/lookup'
9
+ require 'enolib/parse'
10
+ require 'enolib/parser'
11
+ require 'enolib/register'
12
+
13
+ require 'enolib/elements/element_base'
14
+ require 'enolib/elements/value_element_base'
15
+ require 'enolib/elements/section_element'
16
+ require 'enolib/elements/element'
17
+ require 'enolib/elements/empty'
18
+ require 'enolib/elements/field'
19
+ require 'enolib/elements/fieldset_entry'
20
+ require 'enolib/elements/fieldset'
21
+ require 'enolib/elements/list_item'
22
+ require 'enolib/elements/list'
23
+ require 'enolib/elements/section'
24
+
25
+ require 'enolib/elements/missing/missing_element_base'
26
+ require 'enolib/elements/missing/missing_value_element_base'
27
+ require 'enolib/elements/missing/missing_empty'
28
+ require 'enolib/elements/missing/missing_field'
29
+ require 'enolib/elements/missing/missing_fieldset'
30
+ require 'enolib/elements/missing/missing_fieldset_entry'
31
+ require 'enolib/elements/missing/missing_list'
32
+ require 'enolib/elements/missing/missing_section'
33
+ require 'enolib/elements/missing/missing_section_element'
34
+
35
+ require 'enolib/errors/selections'
36
+ require 'enolib/errors/parsing'
37
+ require 'enolib/errors/validation'
38
+
39
+ require 'enolib/reporters/reporter'
40
+ require 'enolib/reporters/html_reporter'
41
+ require 'enolib/reporters/terminal_reporter'
42
+ require 'enolib/reporters/text_reporter'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ HUMAN_INDEXING = 1
5
+ PRETTY_TYPES = {
6
+ document: 'document',
7
+ empty_element: 'empty_element',
8
+ field: 'field',
9
+ fieldset: 'fieldset',
10
+ fieldset_entry: 'fieldset_entry',
11
+ list: 'list',
12
+ list_item: 'list_item',
13
+ multiline_field_begin: 'field',
14
+ section: 'section'
15
+ }
16
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class Context
5
+ attr_accessor :document, :input, :line_count, :messages, :meta, :reporter, :source
6
+
7
+ def initialize(input, locale: Messages::En, reporter: TextReporter, source: nil)
8
+ @input = input
9
+ @messages = locale
10
+ @reporter = reporter
11
+ @source = source
12
+
13
+ @document = {
14
+ elements: [],
15
+ type: :document
16
+ }
17
+
18
+ @meta = []
19
+
20
+ Parser.new(self).run
21
+ end
22
+
23
+ def comment(element)
24
+ unless element.has_key?(:computed_comment)
25
+ element[:computed_comment] =
26
+ if !element.has_key?(:comments)
27
+ nil
28
+ elsif element[:comments].length == 1
29
+ element[:comments].first[:comment]
30
+ else
31
+ first_non_empty_line_index = nil
32
+ last_non_empty_line_index = nil
33
+ shared_indent = Float::INFINITY
34
+
35
+ element[:comments].each_with_index do |comment, index|
36
+ if comment.has_key?(:comment)
37
+ first_non_empty_line_index = index unless first_non_empty_line_index
38
+ last_non_empty_line_index = index
39
+
40
+ indent = comment[:ranges][:comment][RANGE_BEGIN] - comment[:ranges][:line][RANGE_BEGIN]
41
+ shared_indent = indent if indent < shared_indent
42
+ end
43
+ end
44
+
45
+ if first_non_empty_line_index
46
+ non_empty_lines = element[:comments][first_non_empty_line_index..last_non_empty_line_index]
47
+
48
+ non_empty_lines.map do |comment|
49
+ if !comment.has_key?(:comment)
50
+ ''
51
+ elsif (comment[:ranges][:comment][RANGE_BEGIN] - comment[:ranges][:line][RANGE_BEGIN]) == shared_indent
52
+ comment[:comment]
53
+ else
54
+ (' ' * (comment[:ranges][:comment][RANGE_BEGIN] - comment[:ranges][:line][RANGE_BEGIN] - shared_indent)) + comment[:comment]
55
+ end
56
+ end.join("\n")
57
+ else
58
+ nil
59
+ end
60
+ end
61
+ end
62
+
63
+ element[:computed_comment]
64
+ end
65
+
66
+ def elements(section)
67
+ return elements(section[:mirror]) if section.has_key?(:mirror)
68
+
69
+ unless section.has_key?(:computed_elements)
70
+ section[:computed_elements] = section[:elements]
71
+ section[:computed_elements_map] = {}
72
+
73
+ section[:computed_elements].each do |element|
74
+ if section[:computed_elements_map].has_key?(element[:key])
75
+ section[:computed_elements_map][element[:key]].push(element)
76
+ else
77
+ section[:computed_elements_map][element[:key]] = [element]
78
+ end
79
+ end
80
+
81
+ if section.has_key?(:extend)
82
+ copied_elements = elements(section[:extend]).reject { |element| section[:computed_elements_map].has_key?(element[:key]) }
83
+
84
+ section[:computed_elements] = copied_elements + section[:computed_elements]
85
+
86
+ copied_elements.each do |element|
87
+ if section[:computed_elements_map].has_key?(element[:key])
88
+ section[:computed_elements_map][element[:key]].push(element)
89
+ else
90
+ section[:computed_elements_map][element[:key]] = [element]
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ section[:computed_elements]
97
+ end
98
+
99
+ def entries(fieldset)
100
+ return entries(fieldset[:mirror]) if fieldset.has_key?(:mirror)
101
+
102
+ unless fieldset.has_key?(:computed_entries)
103
+ fieldset[:computed_entries] = fieldset[:entries]
104
+ fieldset[:computed_entries_map] = {}
105
+
106
+ fieldset[:computed_entries].each do |entry|
107
+ if fieldset[:computed_entries_map].has_key?(entry[:key])
108
+ fieldset[:computed_entries_map][entry[:key]].push(entry)
109
+ else
110
+ fieldset[:computed_entries_map][entry[:key]] = [entry]
111
+ end
112
+ end
113
+
114
+ if fieldset.has_key?(:extend)
115
+ copied_entries = entries(fieldset[:extend]).reject { |entry| fieldset[:computed_entries_map].has_key?(entry[:key]) }
116
+
117
+ fieldset[:computed_entries] = copied_entries + fieldset[:computed_entries]
118
+
119
+ copied_entries.each do |entry|
120
+ if fieldset[:computed_entries_map].has_key?(entry[:key])
121
+ fieldset[:computed_entries_map][entry[:key]].push(entry)
122
+ else
123
+ fieldset[:computed_entries_map][entry[:key]] = [entry]
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ fieldset[:computed_entries]
130
+ end
131
+
132
+ def items(list)
133
+ if list.has_key?(:mirror)
134
+ items(list[:mirror])
135
+ elsif list.has_key?(:extend)
136
+ items(list[:extend]) + list[:items]
137
+ elsif list.has_key?(:items)
138
+ list[:items]
139
+ else
140
+ []
141
+ end
142
+ end
143
+
144
+ def raw(element)
145
+ # TODO: In other implementations there is only one 'field' type
146
+ # here we get a) symbols, b) including :multiline_field_begin
147
+ result = { type: element[:type] }
148
+
149
+ # TODO: Revisit to think through the case of a present but empty comment
150
+ result[:comment] = comment(element) if element.has_key?(:comments)
151
+
152
+ case element[:type]
153
+ when :empty_element
154
+ result[:key] = element[:key]
155
+ when :field
156
+ result[:key] = element[:key]
157
+ result[:value] = value(element)
158
+ when :list_item
159
+ result[:value] = value(element)
160
+ when :fieldset_entry
161
+ result[:key] = element[:key]
162
+ result[:value] = value(element)
163
+ when :multiline_field_begin
164
+ result[:key] = element[:key]
165
+ result[:value] = value(element)
166
+ when :list
167
+ result[:key] = element[:key]
168
+ result[:items] = items(element).map { |item| raw(item) }
169
+ when :fieldset
170
+ result[:key] = element[:key]
171
+ result[:entries] = entries(element).map { |entry| raw(entry) }
172
+ when :section
173
+ result[:key] = element[:key]
174
+ result[:elements] = elements(element).map { |section_element| raw(section_element) }
175
+ when :document
176
+ result[:elements] = elements(element).map { |section_element| raw(section_element) }
177
+ end
178
+
179
+ result
180
+ end
181
+
182
+ def value(element)
183
+ unless element.has_key?(:computed_value)
184
+ return value(element[:mirror]) if element.has_key?(:mirror)
185
+
186
+ element[:computed_value] = nil
187
+
188
+ if element[:type] == :multiline_field_begin
189
+ if element.has_key?(:lines)
190
+ element[:computed_value] = @input[
191
+ element[:lines][0][:ranges][:line][0]...element[:lines][-1][:ranges][:line][1]
192
+ ]
193
+ end
194
+ else
195
+ element[:computed_value] = element[:value] if element.has_key?(:value)
196
+
197
+ if element.has_key?(:continuations)
198
+ unapplied_spacing = nil
199
+
200
+ element[:continuations].each do |continuation|
201
+ if element[:computed_value] == nil
202
+ element[:computed_value] = continuation[:value] if continuation.has_key?(:value)
203
+ unapplied_spacing = nil
204
+ elsif !continuation.has_key?(:value)
205
+ unapplied_spacing ||= continuation.has_key?(:spaced)
206
+ elsif continuation.has_key?(:spaced) || unapplied_spacing
207
+ element[:computed_value] += ' ' + continuation[:value]
208
+ unapplied_spacing = nil
209
+ else
210
+ element[:computed_value] += continuation[:value]
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ return element[:computed_value]
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class Element < SectionElement
5
+ def to_fieldset_entry
6
+ unless instance_variable_defined?(:@fieldset_entry)
7
+ unless @instruction[:type] == :fieldset_entry
8
+ # TODO: Below and in all implementations - why nil for key as second parameter?
9
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_fieldset_entry')
10
+ end
11
+
12
+ @fieldset_entry = FieldsetEntry.new(@context, @instruction)
13
+ end
14
+
15
+ @fieldset_entry
16
+ end
17
+
18
+ def to_list_item
19
+ unless instance_variable_defined?(:@list_item)
20
+ unless @instruction[:type] == :list_item
21
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_list_item')
22
+ end
23
+
24
+ @list_item = FieldsetEntry.new(@context, @instruction)
25
+ end
26
+
27
+ @list_item
28
+ end
29
+
30
+ def yields_fieldset_entry?
31
+ @instruction[:type] == :fieldset_entry
32
+ end
33
+
34
+ def yields_list_item?
35
+ @instruction[:type] == :list_item
36
+ end
37
+
38
+ def to_s
39
+ "#<Enolib::Element key=#{@instruction[:key]}>"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class ElementBase
5
+ def initialize(context, instruction, parent = nil)
6
+ @context = context
7
+ @instruction = instruction
8
+ @parent = parent
9
+ end
10
+
11
+ def comment_error(message = nil)
12
+ if block_given?
13
+ message = yield(@context.comment(@instruction))
14
+ elsif message.is_a?(Proc)
15
+ message = message.call(@context.comment(@instruction))
16
+ end
17
+
18
+ unless message
19
+ raise ArgumentError.new('A message or message function must be provided')
20
+ end
21
+
22
+ Errors::Validation.comment_error(@context, message, @instruction)
23
+ end
24
+
25
+ def error(message = nil)
26
+ if block_given?
27
+ message = yield(self) # Revisit self in this context - problematic
28
+ elsif message.is_a?(Proc)
29
+ message = message.call(self) # Revisit self in this context - problematic
30
+ end
31
+
32
+ unless message
33
+ raise ArgumentError.new('A message or message function must be provided')
34
+ end
35
+
36
+ Errors::Validation.element_error(@context, message, @instruction)
37
+ end
38
+
39
+ def key(loader = nil)
40
+ loader = Proc.new if block_given?
41
+
42
+ @touched = true
43
+
44
+ unless loader
45
+ raise ArgumentError.new('A loader function must be provided')
46
+ end
47
+
48
+ begin
49
+ loader.call(_key)
50
+ rescue => message
51
+ raise Errors::Validation.key_error(@context, message, @instruction)
52
+ end
53
+ end
54
+
55
+ def key_error(message = nil)
56
+ if block_given?
57
+ message = yield(_key)
58
+ elsif message.is_a?(Proc)
59
+ message = message.call(_key)
60
+ end
61
+
62
+ unless message
63
+ raise ArgumentError.new('A message or message function must be provided')
64
+ end
65
+
66
+ Errors::Validation.key_error(@context, message, @instruction)
67
+ end
68
+
69
+ def optional_comment(loader = nil)
70
+ loader = Proc.new if block_given?
71
+
72
+ unless loader
73
+ raise ArgumentError.new('A loader function must be provided')
74
+ end
75
+
76
+ _comment(loader, required: false)
77
+ end
78
+
79
+ def optional_string_comment()
80
+ _comment(required: false)
81
+ end
82
+
83
+ def raw
84
+ @context.raw(@instruction)
85
+ end
86
+
87
+ def required_comment(loader = nil)
88
+ loader = Proc.new if block_given?
89
+
90
+ unless loader
91
+ raise ArgumentError.new('A loader function must be provided')
92
+ end
93
+
94
+ _comment(loader, required: true)
95
+ end
96
+
97
+ def required_string_comment()
98
+ _comment(required: true)
99
+ end
100
+
101
+ def string_key
102
+ @touched = true
103
+
104
+ _key
105
+ end
106
+
107
+ def touch
108
+ @touched = true
109
+ end
110
+
111
+ private
112
+
113
+ def _comment(loader = nil, required:)
114
+ @touched = true
115
+
116
+ comment = @context.comment(@instruction)
117
+
118
+ if comment
119
+ return comment unless loader
120
+
121
+ begin
122
+ loader.call(comment)
123
+ rescue => message
124
+ raise Errors::Validation.comment_error(@context, message, @instruction)
125
+ end
126
+ else
127
+ return nil unless required
128
+
129
+ raise Errors::Validation.missing_comment(@context, @instruction)
130
+ end
131
+ end
132
+
133
+ def _key
134
+ if @instruction[:type] == :list_item
135
+ @instruction[:parent][:key]
136
+ else
137
+ @instruction[:key]
138
+ end
139
+ end
140
+ end
141
+ end