edn 0.0.0 → 0.9.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.
- data/.gitignore +1 -0
- data/LICENSE +2 -2
- data/README.md +19 -4
- data/Rakefile +5 -0
- data/edn.gemspec +5 -0
- data/lib/edn.rb +50 -1
- data/lib/edn/core_ext.rb +64 -0
- data/lib/edn/parser.rb +140 -0
- data/lib/edn/string_transformer.rb +46 -0
- data/lib/edn/transform.rb +34 -0
- data/lib/edn/type/list.rb +13 -0
- data/lib/edn/type/symbol.rb +37 -0
- data/lib/edn/type/unknown.rb +9 -0
- data/lib/edn/type/uuid.rb +9 -0
- data/lib/edn/types.rb +3 -0
- data/lib/edn/version.rb +1 -1
- data/spec/edn/parser_spec.rb +160 -0
- data/spec/edn/transform_spec.rb +129 -0
- data/spec/edn_spec.rb +83 -0
- data/spec/spec_helper.rb +108 -0
- metadata +85 -4
data/.gitignore
CHANGED
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2012 Clinton
|
1
|
+
Copyright (c) 2012 Relevance Inc & Clinton N. Dreisbach
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# edn-ruby
|
2
2
|
|
3
|
+
© 2012 Relevance Inc and Clinton N. Dreisbach
|
4
|
+
|
3
5
|
**edn-ruby** is a Ruby library to read and write [edn][edn] (extensible data notation), a subset of Clojure used for transferring data between applications, much like JSON, YAML, or XML.
|
4
6
|
|
5
7
|
## Installation
|
@@ -21,7 +23,7 @@ Or install it yourself as:
|
|
21
23
|
To read a string of **edn**:
|
22
24
|
|
23
25
|
```ruby
|
24
|
-
EDN.read(
|
26
|
+
EDN.read('[1 2 {:foo "bar"}]')
|
25
27
|
```
|
26
28
|
|
27
29
|
To convert a data structure to an **edn** string:
|
@@ -32,6 +34,20 @@ data.to_edn
|
|
32
34
|
|
33
35
|
By default, this will work for strings, symbols, numbers, arrays, hashes, sets, nil, Time, and boolean values.
|
34
36
|
|
37
|
+
### Value Translations
|
38
|
+
|
39
|
+
**edn** has some different terminology, and some types that do not map cleanly to Ruby.
|
40
|
+
|
41
|
+
**NOTE**: Comments requested on the following.
|
42
|
+
|
43
|
+
In **edn**, you have _keywords_, which look like Ruby symbols and have the same meaning and purpose. These are converted to Ruby symbols.
|
44
|
+
|
45
|
+
You have **edn** _symbols_, which generally reflect variable names, but have several purposes. We parse these and return `EDN::Type::Symbol` values for them, as they are not directly portable into Ruby.
|
46
|
+
|
47
|
+
You have _vectors_, which map to Ruby arrays, and _lists_, which are linked lists in Clojure. We map these to `EDN::Type::List` values, which are type-compatible with arrays.
|
48
|
+
|
49
|
+
**edn** has character types, but Ruby does not. These are converted into one-character strings.
|
50
|
+
|
35
51
|
### Tagged Values
|
36
52
|
|
37
53
|
The interesting part of **edn** is the _extensible_ part. Data can be be _tagged_ to coerce interpretation of it to a particular data type. An example of a tagged data element:
|
@@ -44,9 +60,9 @@ The tag (`#wolf/pack`) will tell any consumers of this data to use a data type r
|
|
44
60
|
|
45
61
|
The rules for tags from the [**edn** README][README] should be followed. In short, custom tags should have a prefix (the part before the `/`) designating the user that created them or context they are used in. Non-prefixed tags are reserved for built-in tags.
|
46
62
|
|
47
|
-
There are two tags built in by default: `#uuid`, used for UUIDs, and `#inst`, used for an instant in time. In `edn-ruby`, `#inst` is converted to a Time, and Time values are tagged as `#inst`. There is not a UUID data type built into Ruby, so `#uuid` is converted to
|
63
|
+
There are two tags built in by default: `#uuid`, used for UUIDs, and `#inst`, used for an instant in time. In `edn-ruby`, `#inst` is converted to a Time, and Time values are tagged as `#inst`. There is not a UUID data type built into Ruby, so `#uuid` is converted to an instance of `EDN::Type::UUID`.
|
48
64
|
|
49
|
-
Tags that are not registered
|
65
|
+
Tags that are not registered generate a struct of the type `EDN::Type::Unknown` with the methods `tag` and `value`.
|
50
66
|
|
51
67
|
### Registering a New Tag For Reading
|
52
68
|
|
@@ -106,6 +122,5 @@ This method calls `.to_edn` on the second argument and joins the arguments appro
|
|
106
122
|
4. Push to the branch (`git push origin my-new-feature`)
|
107
123
|
5. Create new Pull Request
|
108
124
|
|
109
|
-
|
110
125
|
[edn]: https://github.com/richhickey/edn
|
111
126
|
[README]: https://github.com/richhickey/edn/blob/master/README.md
|
data/Rakefile
CHANGED
data/edn.gemspec
CHANGED
@@ -14,4 +14,9 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = "edn"
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = EDN::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'parslet'
|
19
|
+
gem.add_development_dependency 'pry'
|
20
|
+
gem.add_development_dependency 'rspec'
|
21
|
+
gem.add_development_dependency 'rantly'
|
17
22
|
end
|
data/lib/edn.rb
CHANGED
@@ -1,5 +1,54 @@
|
|
1
|
+
$:.push(File.dirname(__FILE__))
|
1
2
|
require "edn/version"
|
3
|
+
require "edn/core_ext"
|
4
|
+
require "edn/parser"
|
5
|
+
require "edn/transform"
|
2
6
|
|
3
7
|
module EDN
|
4
|
-
|
8
|
+
@parser = EDN::Parser.new
|
9
|
+
@transform = EDN::Transform.new
|
10
|
+
@tags = Hash.new
|
11
|
+
|
12
|
+
def self.read(edn)
|
13
|
+
@transform.apply(@parser.parse(edn))
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.register(tag, func = nil, &block)
|
17
|
+
if block_given?
|
18
|
+
func = block
|
19
|
+
end
|
20
|
+
|
21
|
+
raise "EDN.register requires a block or callable." if func.nil?
|
22
|
+
|
23
|
+
if func.is_a?(Class)
|
24
|
+
@tags[tag] = lambda { |*args| func.new(*args) }
|
25
|
+
else
|
26
|
+
@tags[tag] = func
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.unregister(tag)
|
31
|
+
@tags[tag] = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.tag_value(tag, value)
|
35
|
+
func = @tags[tag]
|
36
|
+
if func
|
37
|
+
func.call(value)
|
38
|
+
else
|
39
|
+
EDN::Type::Unknown.new(tag, value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.tagout(tag, value)
|
44
|
+
["##{tag}", value.to_edn].join(" ")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
EDN.register("inst") do |value|
|
49
|
+
Time.parse(value)
|
50
|
+
end
|
51
|
+
|
52
|
+
EDN.register("uuid") do |value|
|
53
|
+
EDN::Type::UUID.new(value)
|
5
54
|
end
|
data/lib/edn/core_ext.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module EDN
|
4
|
+
module CoreExt
|
5
|
+
module Unquoted
|
6
|
+
def to_edn
|
7
|
+
self.to_s
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module String
|
12
|
+
def to_edn
|
13
|
+
self.inspect
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Symbol
|
18
|
+
def to_edn
|
19
|
+
":#{self.to_s}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Array
|
24
|
+
def to_edn
|
25
|
+
'[' + self.map(&:to_edn).join(" ") + ']'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Hash
|
30
|
+
def to_edn
|
31
|
+
'{' + self.map { |k, v| [k, v].map(&:to_edn).join(" ") }.join(", ") + '}'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Set
|
36
|
+
def to_edn
|
37
|
+
'#{' + self.to_a.map(&:to_edn).join(" ") + '}'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module NilClass
|
42
|
+
def to_edn
|
43
|
+
"nil"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Time
|
48
|
+
def to_edn
|
49
|
+
EDN.tagout("inst", self.xmlschema)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Numeric.send(:include, EDN::CoreExt::Unquoted)
|
56
|
+
TrueClass.send(:include, EDN::CoreExt::Unquoted)
|
57
|
+
FalseClass.send(:include, EDN::CoreExt::Unquoted)
|
58
|
+
NilClass.send(:include, EDN::CoreExt::NilClass)
|
59
|
+
String.send(:include, EDN::CoreExt::String)
|
60
|
+
Symbol.send(:include, EDN::CoreExt::Symbol)
|
61
|
+
Array.send(:include, EDN::CoreExt::Array)
|
62
|
+
Hash.send(:include, EDN::CoreExt::Hash)
|
63
|
+
Set.send(:include, EDN::CoreExt::Set)
|
64
|
+
Time.send(:include, EDN::CoreExt::Time)
|
data/lib/edn/parser.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module EDN
|
4
|
+
class Parser < Parslet::Parser
|
5
|
+
root(:top)
|
6
|
+
|
7
|
+
rule(:top) {
|
8
|
+
space? >> value >> space?
|
9
|
+
}
|
10
|
+
|
11
|
+
rule(:value) {
|
12
|
+
tagged_value | base_value
|
13
|
+
}
|
14
|
+
|
15
|
+
rule(:base_value) {
|
16
|
+
vector |
|
17
|
+
list |
|
18
|
+
set |
|
19
|
+
map |
|
20
|
+
boolean |
|
21
|
+
str('nil').as(:nil) |
|
22
|
+
keyword |
|
23
|
+
string |
|
24
|
+
character |
|
25
|
+
float |
|
26
|
+
integer |
|
27
|
+
symbol
|
28
|
+
}
|
29
|
+
|
30
|
+
rule(:tagged_value) {
|
31
|
+
(tag >> space >> base_value.as(:value)).as(:tagged_value)
|
32
|
+
}
|
33
|
+
|
34
|
+
# Collections
|
35
|
+
|
36
|
+
rule(:vector) {
|
37
|
+
str('[') >>
|
38
|
+
top.repeat.as(:vector) >>
|
39
|
+
space? >>
|
40
|
+
str(']')
|
41
|
+
}
|
42
|
+
|
43
|
+
rule(:list) {
|
44
|
+
str('(') >>
|
45
|
+
top.repeat.as(:list) >>
|
46
|
+
space? >>
|
47
|
+
str(')')
|
48
|
+
}
|
49
|
+
|
50
|
+
rule(:set) {
|
51
|
+
str('#{') >>
|
52
|
+
top.repeat.as(:set) >>
|
53
|
+
space? >>
|
54
|
+
str('}')
|
55
|
+
}
|
56
|
+
|
57
|
+
rule(:map) {
|
58
|
+
str('{') >>
|
59
|
+
(top.as(:key) >> top.as(:value)).repeat.as(:map) >>
|
60
|
+
space? >>
|
61
|
+
str('}')
|
62
|
+
}
|
63
|
+
|
64
|
+
# Primitives
|
65
|
+
|
66
|
+
rule(:integer) {
|
67
|
+
(str('-').maybe >>
|
68
|
+
(str('0') | match('[1-9]') >> digit.repeat)).as(:integer) >>
|
69
|
+
str('N').maybe
|
70
|
+
}
|
71
|
+
|
72
|
+
rule(:float) {
|
73
|
+
(str('-').maybe >>
|
74
|
+
(str('0') | (match('[1-9]') >> digit.repeat)) >>
|
75
|
+
str('.') >> digit.repeat(1) >>
|
76
|
+
(match('[eE]') >> match('[\-+]').maybe >> digit.repeat).maybe).as(:float) >>
|
77
|
+
str('M').maybe
|
78
|
+
}
|
79
|
+
|
80
|
+
rule(:string) {
|
81
|
+
str('"') >>
|
82
|
+
(str('\\') >> any | str('"').absent? >> any).repeat.as(:string) >>
|
83
|
+
str('"')
|
84
|
+
}
|
85
|
+
|
86
|
+
rule(:character) {
|
87
|
+
str("\\") >>
|
88
|
+
(str('newline') | str('space') | str('tab') |
|
89
|
+
match['[:graph:]']).as(:character)
|
90
|
+
}
|
91
|
+
|
92
|
+
rule(:keyword) {
|
93
|
+
str(':') >> symbol.as(:keyword)
|
94
|
+
}
|
95
|
+
|
96
|
+
rule(:symbol) {
|
97
|
+
(symbol_chars >> (str('/') >> symbol_chars).maybe |
|
98
|
+
str('/')).as(:symbol)
|
99
|
+
}
|
100
|
+
|
101
|
+
rule(:boolean) {
|
102
|
+
str('true').as(:true) | str('false').as(:false)
|
103
|
+
}
|
104
|
+
|
105
|
+
# Parts
|
106
|
+
|
107
|
+
rule(:tag) {
|
108
|
+
(str('#') >> symbol).as(:tag)
|
109
|
+
}
|
110
|
+
|
111
|
+
rule(:symbol_chars) {
|
112
|
+
(symbol_first_char >>
|
113
|
+
valid_chars.repeat) |
|
114
|
+
match['\-\.']
|
115
|
+
}
|
116
|
+
|
117
|
+
rule(:symbol_first_char) {
|
118
|
+
(match['\-\.'] >> match['0-9'].absent? |
|
119
|
+
match['\#\:0-9'].absent?) >> valid_chars
|
120
|
+
}
|
121
|
+
|
122
|
+
rule(:valid_chars) {
|
123
|
+
match['[:alnum:]'] | sym_punct
|
124
|
+
}
|
125
|
+
|
126
|
+
rule(:sym_punct) {
|
127
|
+
match['\.\*\+\!\-\?\:\#\_']
|
128
|
+
}
|
129
|
+
|
130
|
+
rule(:digit) {
|
131
|
+
match['0-9']
|
132
|
+
}
|
133
|
+
|
134
|
+
rule(:space) {
|
135
|
+
match('[\s,]').repeat(1)
|
136
|
+
}
|
137
|
+
|
138
|
+
rule(:space?) { space.maybe }
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EDN
|
2
|
+
module StringTransformer
|
3
|
+
# Unescape characters in strings.
|
4
|
+
# Borrowed from json-pure gem.
|
5
|
+
UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
|
6
|
+
UNESCAPE_MAP.update({ ?" => '"',
|
7
|
+
?\\ => '\\',
|
8
|
+
?/ => '/',
|
9
|
+
#' Clear messed up syntax highlighting with Emacs.
|
10
|
+
?b => "\b",
|
11
|
+
?f => "\f",
|
12
|
+
?n => "\n",
|
13
|
+
?r => "\r",
|
14
|
+
?t => "\t",
|
15
|
+
?u => nil,
|
16
|
+
})
|
17
|
+
|
18
|
+
EMPTY_8BIT_STRING = ''
|
19
|
+
if ::String.method_defined?(:encode)
|
20
|
+
EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_string(string)
|
24
|
+
string = string.to_s
|
25
|
+
return '' if string.empty?
|
26
|
+
string = string.gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c|
|
27
|
+
#" Clear messed up syntax highlighting with Emacs.
|
28
|
+
if u = UNESCAPE_MAP[$&[1]]
|
29
|
+
u
|
30
|
+
else # \uXXXX
|
31
|
+
bytes = EMPTY_8BIT_STRING.dup
|
32
|
+
i = 0
|
33
|
+
while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
|
34
|
+
bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
|
35
|
+
i += 1
|
36
|
+
end
|
37
|
+
JSON.iconv('utf-8', 'utf-16be', bytes)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
if string.respond_to?(:force_encoding)
|
41
|
+
string.force_encoding(::Encoding::UTF_8)
|
42
|
+
end
|
43
|
+
string
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'edn/string_transformer'
|
2
|
+
require 'edn/types'
|
3
|
+
|
4
|
+
module EDN
|
5
|
+
class Transform < Parslet::Transform
|
6
|
+
rule(:true => simple(:x)) { true }
|
7
|
+
rule(:false => simple(:x)) { false }
|
8
|
+
rule(:nil => simple(:x)) { nil }
|
9
|
+
|
10
|
+
rule(:integer => simple(:x)) { Integer(x) }
|
11
|
+
rule(:float => simple(:x)) { Float(x) }
|
12
|
+
|
13
|
+
rule(:string => simple(:x)) { EDN::StringTransformer.parse_string(x) }
|
14
|
+
rule(:keyword => simple(:x)) { x.to_sym }
|
15
|
+
rule(:symbol => simple(:x)) { EDN::Type::Symbol.new(x) }
|
16
|
+
rule(:character => simple(:x)) {
|
17
|
+
case x
|
18
|
+
when "newline" then "\n"
|
19
|
+
when "tab" then "\t"
|
20
|
+
when "space" then " "
|
21
|
+
else x.to_s
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
rule(:vector => subtree(:array)) { array }
|
26
|
+
rule(:list => subtree(:array)) { EDN::Type::List.new(*array) }
|
27
|
+
rule(:set => subtree(:array)) { Set.new(array) }
|
28
|
+
rule(:map => subtree(:array)) { Hash[array.map { |hash| [hash[:key], hash[:value]] }] }
|
29
|
+
|
30
|
+
rule(:tagged_value => subtree(:x)) {
|
31
|
+
EDN.tag_value(x[:tag].to_s, x[:value])
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EDN
|
2
|
+
module Type
|
3
|
+
class Symbol
|
4
|
+
attr_reader :symbol
|
5
|
+
|
6
|
+
def initialize(sym)
|
7
|
+
@symbol = sym.to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
return false unless other.is_a?(Symbol)
|
12
|
+
to_sym == other.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def eql?(other)
|
16
|
+
return false unless other.is_a?(Symbol)
|
17
|
+
to_sym == other.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash
|
21
|
+
@symbol.hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_sym
|
25
|
+
@symbol
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
@symbol.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_edn
|
33
|
+
@symbol.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/edn/types.rb
ADDED
data/lib/edn/version.rb
CHANGED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EDN::Parser do
|
4
|
+
include RantlyHelpers
|
5
|
+
|
6
|
+
let(:parser) { EDN::Parser.new }
|
7
|
+
|
8
|
+
context "value" do
|
9
|
+
it "should consume nil" do
|
10
|
+
parser.value.should parse("nil")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "boolean" do
|
15
|
+
it "should consume true" do
|
16
|
+
parser.boolean.should parse("true")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should consume false" do
|
20
|
+
parser.boolean.should parse("false")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "integer" do
|
25
|
+
it "should consume integers" do
|
26
|
+
rant(RantlyHelpers::INTEGER).each do |int|
|
27
|
+
parser.integer.should parse(int)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "float" do
|
33
|
+
it "should consume simple floats" do
|
34
|
+
rant(RantlyHelpers::FLOAT).each do |float|
|
35
|
+
parser.float.should parse(float)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should consume floats with exponents" do
|
40
|
+
rant(RantlyHelpers::FLOAT_WITH_EXP).each do |float|
|
41
|
+
parser.float.should parse(float)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "symbol" do
|
47
|
+
it "should consume any symbols" do
|
48
|
+
rant(RantlyHelpers::SYMBOL).each do |symbol|
|
49
|
+
parser.symbol.should parse("#{symbol}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "special cases" do
|
54
|
+
it "should consume '/'" do
|
55
|
+
parser.symbol.should parse('/')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should consume '.'" do
|
59
|
+
parser.symbol.should parse('.')
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should consume '-'" do
|
63
|
+
parser.symbol.should parse('-')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "keyword" do
|
69
|
+
it "should consume any keywords" do
|
70
|
+
rant(RantlyHelpers::SYMBOL).each do |symbol|
|
71
|
+
parser.keyword.should parse(":#{symbol}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "tag" do
|
77
|
+
it "should consume any tags" do
|
78
|
+
rant(RantlyHelpers::SYMBOL).each do |symbol|
|
79
|
+
parser.tag.should parse("##{symbol}")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "string" do
|
85
|
+
it "should consume any string" do
|
86
|
+
rant(RantlyHelpers::STRING).each do |string|
|
87
|
+
parser.string.should parse(string)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "character" do
|
93
|
+
it "should consume any character" do
|
94
|
+
rant(RantlyHelpers::CHARACTER).each do |char|
|
95
|
+
parser.character.should parse(char)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "vector" do
|
101
|
+
it "should consume an empty vector" do
|
102
|
+
parser.vector.should parse('[]')
|
103
|
+
parser.vector.should parse('[ ]')
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should consume vectors of mixed elements" do
|
107
|
+
rant(RantlyHelpers::VECTOR).each do |vector|
|
108
|
+
parser.vector.should parse(vector)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "list" do
|
114
|
+
it "should consume an empty list" do
|
115
|
+
parser.list.should parse('()')
|
116
|
+
parser.list.should parse('( )')
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should consume lists of mixed elements" do
|
120
|
+
rant(RantlyHelpers::LIST).each do |list|
|
121
|
+
parser.list.should parse(list)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "set" do
|
127
|
+
it "should consume an empty set" do
|
128
|
+
parser.set.parse_with_debug('#{}')
|
129
|
+
parser.set.should parse('#{}')
|
130
|
+
parser.set.should parse('#{ }')
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should consume sets of mixed elements" do
|
134
|
+
rant(RantlyHelpers::SET).each do |set|
|
135
|
+
parser.set.should parse(set)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "map" do
|
141
|
+
it "should consume an empty map" do
|
142
|
+
parser.map.should parse('{}')
|
143
|
+
parser.map.should parse('{ }')
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should consume maps of mixed elements" do
|
147
|
+
rant(RantlyHelpers::MAP).each do |map|
|
148
|
+
parser.map.should parse(map)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "tagged value" do
|
154
|
+
it "should consume tagged values" do
|
155
|
+
rant(RantlyHelpers::TAGGED_VALUE).each do |value|
|
156
|
+
parser.tagged_value.should parse(value)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EDN::Transform do
|
4
|
+
context "integer" do
|
5
|
+
it "should emit an integer" do
|
6
|
+
subject.apply(:integer => "1").should == 1
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
context "float" do
|
11
|
+
it "should emit an float" do
|
12
|
+
subject.apply(:float => "1.0").should == 1.0
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "string" do
|
17
|
+
it "should emit a string with control characters substituted" do
|
18
|
+
subject.apply(:string => 'hello\nworld').should == "hello\nworld"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not evaluate interpolated Ruby code" do
|
22
|
+
subject.apply(:string => 'hello\n#{world}').should == "hello\n\#{world}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "keyword" do
|
27
|
+
it "should emit a Ruby symbol" do
|
28
|
+
subject.apply(:keyword => "test").should == :test
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "symbol" do
|
33
|
+
it "should emit an EDN symbol" do
|
34
|
+
subject.apply(:symbol => "test").should == EDN::Type::Symbol.new('test')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "boolean" do
|
39
|
+
it "should emit true or false" do
|
40
|
+
subject.apply(:true => "true").should == true
|
41
|
+
subject.apply(:false => "false").should == false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "nil" do
|
46
|
+
it "should emit nil" do
|
47
|
+
subject.apply(:nil => "nil").should == nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "character" do
|
52
|
+
it "should emit a string" do
|
53
|
+
subject.apply(:character => "&").should == "&"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should handle newline, space, and tab special cases" do
|
57
|
+
subject.apply(:character => "newline").should == "\n"
|
58
|
+
subject.apply(:character => "space").should == " "
|
59
|
+
subject.apply(:character => "tab").should == "\t"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "vector" do
|
64
|
+
it "should emit an array" do
|
65
|
+
subject.apply(:vector => []).should == []
|
66
|
+
subject.apply(:vector => [{:integer => "1"}, {:string => "abc"}]).should == [1, "abc"]
|
67
|
+
subject.apply(:vector => [{:vector => [{:integer => "1"}, {:string => "abc"}]}, {:float => "3.14"}]).should == [[1, "abc"], 3.14]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "list" do
|
72
|
+
it "should emit a list" do
|
73
|
+
subject.apply(:list => []).should == EDN::Type::List.new
|
74
|
+
subject.apply(:list => [{:integer => "1"}, {:string => "abc"}]).should == EDN::Type::List.new(1, "abc")
|
75
|
+
subject.apply(:list => [{:list => [{:integer => "1"}, {:string => "abc"}]}, {:float => "3.14"}]).should == \
|
76
|
+
EDN::Type::List.new(EDN::Type::List.new(1, "abc"), 3.14)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should be type-compatible with arrays" do
|
80
|
+
subject.apply(:list => [{:integer => "1"}, {:string => "abc"}]).should == [1, "abc"]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "set" do
|
85
|
+
it "should emit a set" do
|
86
|
+
subject.apply(:set => []).should == Set.new
|
87
|
+
subject.apply(:set => [1, "abc", 2]).should == Set.new([1, "abc", 2])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "map" do
|
92
|
+
it "should emit a hash" do
|
93
|
+
map_tree = {:map=>
|
94
|
+
[ {:key=>{:keyword=>{:symbol=>"a"}}, :value=>{:integer=>"1"}},
|
95
|
+
{:key=>{:keyword=>{:symbol=>"b"}}, :value=>{:integer=>"2"}}
|
96
|
+
]
|
97
|
+
}
|
98
|
+
|
99
|
+
subject.apply(map_tree).should == {:a => 1, :b => 2}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should work with nested maps" do
|
103
|
+
map_tree = {:map=>
|
104
|
+
[{:key=>{:keyword=>{:symbol=>"a"}}, :value=>{:integer=>"1"}},
|
105
|
+
{:key=>{:keyword=>{:symbol=>"b"}}, :value=>{:integer=>"2"}},
|
106
|
+
{:key=>
|
107
|
+
{:map=>
|
108
|
+
[{:key=>{:keyword=>{:symbol=>"c"}}, :value=>{:integer=>"3"}}]},
|
109
|
+
:value=>{:integer=>"4"}}]}
|
110
|
+
subject.apply(map_tree).should == {:a => 1, :b => 2, {:c => 3} => 4}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "tagged value" do
|
115
|
+
it "should emit a EDN::Type::Unknown if the tag is not registered" do
|
116
|
+
subject.apply(:tagged_value => {
|
117
|
+
:tag => 'uri', :value => {:string => 'http://google.com'}
|
118
|
+
}).should == EDN::Type::Unknown.new("uri", "http://google.com")
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should emit the transformed value if the tag is registered" do
|
122
|
+
EDN.register("uri", lambda { |uri| URI(uri) })
|
123
|
+
subject.apply(:tagged_value => {
|
124
|
+
:tag => 'uri', :value => {:string => 'http://google.com'}
|
125
|
+
}).should == URI("http://google.com")
|
126
|
+
EDN.unregister("uri") # cleanup
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/spec/edn_spec.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EDN do
|
4
|
+
include RantlyHelpers
|
5
|
+
|
6
|
+
context "#read" do
|
7
|
+
it "reads single values" do
|
8
|
+
EDN.read("1").should == 1
|
9
|
+
EDN.read("3.14").should == 3.14
|
10
|
+
EDN.read('"hello\nworld"').should == "hello\nworld"
|
11
|
+
EDN.read(':hello').should == :hello
|
12
|
+
EDN.read(':hello/world').should == :"hello/world"
|
13
|
+
EDN.read('hello').should == EDN::Type::Symbol.new('hello')
|
14
|
+
EDN.read('hello/world').should == EDN::Type::Symbol.new('hello/world')
|
15
|
+
EDN.read('true').should == true
|
16
|
+
EDN.read('false').should == false
|
17
|
+
EDN.read('nil').should == nil
|
18
|
+
EDN.read('\c').should == "c"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "reads #inst tagged values" do
|
22
|
+
EDN.read('#inst "2012-09-10T16:16:03-04:00"').should == Time.new(2012, 9, 10, 16, 16, 3, '-04:00')
|
23
|
+
end
|
24
|
+
|
25
|
+
it "reads vectors" do
|
26
|
+
EDN.read('[]').should == []
|
27
|
+
EDN.read('[1]').should == [1]
|
28
|
+
EDN.read('["hello" 1 2]').should == ['hello', 1, 2]
|
29
|
+
EDN.read('[[1 [:hi]]]').should == [[1, [:hi]]]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "reads lists" do
|
33
|
+
EDN.read('()').should == []
|
34
|
+
EDN.read('(1)').should == [1]
|
35
|
+
EDN.read('("hello" 1 2)').should == ['hello', 1, 2]
|
36
|
+
EDN.read('((1 (:hi)))').should == [[1, [:hi]]]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "reads maps" do
|
40
|
+
EDN.read('{}').should == {}
|
41
|
+
EDN.read('{:a :b}').should == {:a => :b}
|
42
|
+
EDN.read('{:a 1, :b 2}').should == {:a => 1, :b => 2}
|
43
|
+
EDN.read('{:a {:b :c}}').should == {:a => {:b => :c}}
|
44
|
+
end
|
45
|
+
|
46
|
+
it "reads sets" do
|
47
|
+
EDN.read('#{}').should == Set.new
|
48
|
+
EDN.read('#{1}').should == Set[1]
|
49
|
+
EDN.read('#{1 "abc"}').should == Set[1, "abc"]
|
50
|
+
EDN.read('#{1 #{:abc}}').should == Set[1, Set[:abc]]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "reads any valid value" do
|
54
|
+
values = rant(RantlyHelpers::VALUE)
|
55
|
+
values.each do |value|
|
56
|
+
if value == "nil"
|
57
|
+
EDN.read(value).should be_nil
|
58
|
+
else
|
59
|
+
EDN.read(value).should_not be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "writing" do
|
66
|
+
it "writes any valid value" do
|
67
|
+
values = rant(RantlyHelpers::VALUE)
|
68
|
+
values.each do |value|
|
69
|
+
expect {
|
70
|
+
EDN.read(value).to_edn
|
71
|
+
}.to_not raise_error
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "writes equivalent edn to what it reads" do
|
76
|
+
values = rant(RantlyHelpers::VALUE)
|
77
|
+
values.each do |value|
|
78
|
+
ruby_value = EDN.read(value)
|
79
|
+
ruby_value.should == EDN.read(ruby_value.to_edn)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'edn'
|
3
|
+
require 'parslet/rig/rspec'
|
4
|
+
require 'parslet/convenience'
|
5
|
+
require 'rantly'
|
6
|
+
|
7
|
+
REPEAT = (ENV["REPEAT"] || 100).to_i
|
8
|
+
|
9
|
+
RSpec.configure do |c|
|
10
|
+
c.fail_fast = true
|
11
|
+
end
|
12
|
+
|
13
|
+
module RantlyHelpers
|
14
|
+
|
15
|
+
SYMBOL = lambda { |_|
|
16
|
+
branch(PLAIN_SYMBOL, NAMESPACED_SYMBOL)
|
17
|
+
}
|
18
|
+
|
19
|
+
PLAIN_SYMBOL = lambda { |_|
|
20
|
+
sized(range(1, 100)) {
|
21
|
+
s = string(/[[:alnum:]]|[\-\.\.\*\+\!\-\?\:\#\_]/)
|
22
|
+
guard s =~ /^[A-Za-z\-\.\.\*\+\!\-\?\:\#\_]/
|
23
|
+
guard s !~ /^[\-\.][0-9]/
|
24
|
+
guard s !~ /^[\:\#]/
|
25
|
+
s
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
NAMESPACED_SYMBOL = lambda { |_|
|
30
|
+
[call(PLAIN_SYMBOL), call(PLAIN_SYMBOL)].join("/")
|
31
|
+
}
|
32
|
+
|
33
|
+
INTEGER = lambda { |_| integer.to_s }
|
34
|
+
|
35
|
+
STRING = lambda { |_| sized(range(1, 100)) { string.inspect } }
|
36
|
+
|
37
|
+
FLOAT = lambda { |_| (float * range(-1000, 1000)).to_s }
|
38
|
+
|
39
|
+
FLOAT_WITH_EXP = lambda { |_|
|
40
|
+
# limited range because of Infinity
|
41
|
+
[float, choose("e", "E", "e+", "E+", "e-", "e+"), range(1, 100)].
|
42
|
+
map(&:to_s).
|
43
|
+
join("")
|
44
|
+
}
|
45
|
+
|
46
|
+
CHARACTER = lambda { |_|
|
47
|
+
"\\" +
|
48
|
+
sized(1) {
|
49
|
+
freq([1, [:choose, "newline", "space", "tab"]],
|
50
|
+
[5, [:string, :graph]])
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
BOOL_OR_NIL = lambda { |_|
|
55
|
+
choose("true", "false", "nil")
|
56
|
+
}
|
57
|
+
|
58
|
+
ARRAY = lambda { |_|
|
59
|
+
array(range(0, 10)) { call(VALUE) }
|
60
|
+
}
|
61
|
+
|
62
|
+
VECTOR = lambda { |_|
|
63
|
+
"[" + call(ARRAY).join(" ") + "]"
|
64
|
+
}
|
65
|
+
|
66
|
+
LIST = lambda { |_|
|
67
|
+
"(" + call(ARRAY).join(" ") + ")"
|
68
|
+
}
|
69
|
+
|
70
|
+
SET = lambda { |_|
|
71
|
+
'#{' + call(ARRAY).join(" ") + '}'
|
72
|
+
}
|
73
|
+
|
74
|
+
MAP = lambda { |_|
|
75
|
+
size = range(0, 10)
|
76
|
+
keys = array(size) { call(VALUE) }
|
77
|
+
values = array(size) { call(VALUE) }
|
78
|
+
arrays = keys.zip(values)
|
79
|
+
'{' + arrays.map { |array| array.join(" ") }.join(", ") + '}'
|
80
|
+
}
|
81
|
+
|
82
|
+
VALUE = lambda { |_|
|
83
|
+
freq([10, BASIC_VALUE],
|
84
|
+
[1, TAGGED_VALUE])
|
85
|
+
}
|
86
|
+
|
87
|
+
BASIC_VALUE = lambda { |_|
|
88
|
+
branch(INTEGER,
|
89
|
+
FLOAT,
|
90
|
+
FLOAT_WITH_EXP,
|
91
|
+
STRING,
|
92
|
+
SYMBOL,
|
93
|
+
CHARACTER,
|
94
|
+
BOOL_OR_NIL,
|
95
|
+
VECTOR,
|
96
|
+
LIST,
|
97
|
+
SET,
|
98
|
+
MAP)
|
99
|
+
}
|
100
|
+
|
101
|
+
TAGGED_VALUE = lambda { |_|
|
102
|
+
"#" + [call(SYMBOL), call(BASIC_VALUE)].join(" ")
|
103
|
+
}
|
104
|
+
|
105
|
+
def rant(fun, count = REPEAT)
|
106
|
+
Rantly(count) { call(fun) }
|
107
|
+
end
|
108
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: edn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,72 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-09-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: parslet
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: pry
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rantly
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
14
78
|
description: ! '''edn implements a reader for Extensible Data Notation by Rich Hickey.'''
|
15
79
|
email:
|
16
80
|
- clinton@thinkrelevance.com
|
@@ -25,7 +89,20 @@ files:
|
|
25
89
|
- Rakefile
|
26
90
|
- edn.gemspec
|
27
91
|
- lib/edn.rb
|
92
|
+
- lib/edn/core_ext.rb
|
93
|
+
- lib/edn/parser.rb
|
94
|
+
- lib/edn/string_transformer.rb
|
95
|
+
- lib/edn/transform.rb
|
96
|
+
- lib/edn/type/list.rb
|
97
|
+
- lib/edn/type/symbol.rb
|
98
|
+
- lib/edn/type/unknown.rb
|
99
|
+
- lib/edn/type/uuid.rb
|
100
|
+
- lib/edn/types.rb
|
28
101
|
- lib/edn/version.rb
|
102
|
+
- spec/edn/parser_spec.rb
|
103
|
+
- spec/edn/transform_spec.rb
|
104
|
+
- spec/edn_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
29
106
|
homepage: ''
|
30
107
|
licenses: []
|
31
108
|
post_install_message:
|
@@ -50,4 +127,8 @@ rubygems_version: 1.8.23
|
|
50
127
|
signing_key:
|
51
128
|
specification_version: 3
|
52
129
|
summary: ! '''edn implements a reader for Extensible Data Notation by Rich Hickey.'''
|
53
|
-
test_files:
|
130
|
+
test_files:
|
131
|
+
- spec/edn/parser_spec.rb
|
132
|
+
- spec/edn/transform_spec.rb
|
133
|
+
- spec/edn_spec.rb
|
134
|
+
- spec/spec_helper.rb
|