edn 0.0.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|