muskox 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +9 -0
- data/lib/muskox.rb +165 -0
- data/lib/muskox/json_lexer.rb +336 -0
- data/lib/muskox/version.rb +3 -0
- data/muskox.gemspec +23 -0
- data/spec/json_schema_spec.rb +35 -0
- data/spec/muskox_spec.rb +278 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3510d3fe36c918d6bd4d03adcb3c0e7c2d97b483
|
4
|
+
data.tar.gz: c363d6428df50d83487ef2225f7a0406ee070a44
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: aeed441cc11da57916011743180f57bbae7e558aed19e5b1580858674b379e0de610602299d611b345b658e169bc4a13b42f755be1ba7cbf47359179500a946a
|
7
|
+
data.tar.gz: 5a9120f14d48e687e2fd836bb85bd83bc93de4ef3bf5a90543f420056ba7e6129dbb33e597f3ad55719b49a8f2014d374627c1ae98a6b648ab4ece611e82def9
|
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Nick Howard
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Muskox
|
2
|
+
|
3
|
+
A JSON Parser-Generator that takes a json-schema and converts it into a parser.
|
4
|
+
|
5
|
+
## Why?
|
6
|
+
|
7
|
+
Using a parser to handle inputs makes your app safe from attacks that rely on passing disallowed params, because disallowed params will either be ignored or rejected.
|
8
|
+
|
9
|
+
> Be definite about what you accept.(*)
|
10
|
+
>
|
11
|
+
> Treat inputs as a language, accept it with a matching computational
|
12
|
+
> power, generate its recognizer from its grammar.
|
13
|
+
>
|
14
|
+
> Treat input-handling computational power as privilege, and reduce it
|
15
|
+
> whenever possible.
|
16
|
+
|
17
|
+
http://www.cs.dartmouth.edu/~sergey/langsec/postel-principle-patch.txt
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
gem 'muskox'
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install muskox
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# to generate a parser, call generate w/ a JSON-Schema
|
37
|
+
parser = Muskox.generate({
|
38
|
+
"title" => "Schema",
|
39
|
+
"type" => "object",
|
40
|
+
"properties" => {
|
41
|
+
"number" => {
|
42
|
+
"type" => "integer"
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"required" => ["number"]
|
46
|
+
})
|
47
|
+
|
48
|
+
# then call parse with the string you want to have parsed
|
49
|
+
n = parser.parse "{\"number\": 1}"
|
50
|
+
# => {"number" => 1}
|
51
|
+
|
52
|
+
# invalid types are disallowed
|
53
|
+
parser.parse "{\"number\": true}" rescue puts $!
|
54
|
+
```
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
1. Fork it
|
59
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
60
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
61
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
62
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/muskox.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require "muskox/version"
|
2
|
+
require "muskox/json_lexer"
|
3
|
+
|
4
|
+
module Muskox
|
5
|
+
def self.generate schema
|
6
|
+
Parser.new schema
|
7
|
+
end
|
8
|
+
|
9
|
+
class Parser
|
10
|
+
ROOT = nil
|
11
|
+
attr_reader :schema
|
12
|
+
def initialize schema
|
13
|
+
@schema = schema
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse input
|
17
|
+
r = nil
|
18
|
+
schema_stack = [schema]
|
19
|
+
stack = [[ROOT, ROOT]]
|
20
|
+
lexer = Pure::Lexer.new input, :quirks_mode => true
|
21
|
+
lexer.lex do |type, value|
|
22
|
+
# puts "token #{type}: #{value}"
|
23
|
+
# puts "stack #{stack.inspect}"
|
24
|
+
# puts "schema stack #{schema_stack.last["type"]}"
|
25
|
+
case type
|
26
|
+
when :property
|
27
|
+
if schema_stack.last["properties"] && schema_stack.last["properties"].keys.include?(value)
|
28
|
+
stack.push [type, value]
|
29
|
+
else
|
30
|
+
raise ParserError, "Unexpected property: #{value}"
|
31
|
+
end
|
32
|
+
when :array_begin
|
33
|
+
case stack.last.first
|
34
|
+
when :property
|
35
|
+
last = stack.last
|
36
|
+
matching_type expected_type(schema_stack.last, last), "array" do
|
37
|
+
stack.push [:array, []]
|
38
|
+
schema_stack.push(schema["properties"][last.last])
|
39
|
+
end
|
40
|
+
when ROOT
|
41
|
+
stack.push [:array, []]
|
42
|
+
else
|
43
|
+
raise "unknown stack type #{stack.last}"
|
44
|
+
end
|
45
|
+
when :array_end
|
46
|
+
array_top = stack.pop
|
47
|
+
|
48
|
+
case stack.last.first
|
49
|
+
when :property
|
50
|
+
schema_stack.pop
|
51
|
+
last = stack.pop
|
52
|
+
matching_type expected_type(schema_stack.last, last), "array" do
|
53
|
+
stack.last.last[last.last] = array_top.last
|
54
|
+
end
|
55
|
+
when :array
|
56
|
+
matching_type expected_type(schema_stack.last, last), "array" do
|
57
|
+
stack.last.last << array_top.last
|
58
|
+
end
|
59
|
+
when ROOT
|
60
|
+
matching_type schema_stack.last["type"], "array" do
|
61
|
+
r = stack.last.last
|
62
|
+
end
|
63
|
+
else
|
64
|
+
raise "unknown stack type #{stack.last}"
|
65
|
+
end
|
66
|
+
when :object_begin
|
67
|
+
case stack.last.first
|
68
|
+
when :property
|
69
|
+
last = stack.last
|
70
|
+
matching_type expected_type(schema_stack.last, last), "object" do
|
71
|
+
stack.push [:object, {}]
|
72
|
+
schema_stack.push(schema["properties"][last.last])
|
73
|
+
end
|
74
|
+
when ROOT
|
75
|
+
stack.push [:object, {}]
|
76
|
+
end
|
77
|
+
when :object_end
|
78
|
+
object_top = stack.pop
|
79
|
+
|
80
|
+
if schema_stack.last["required"] && !(schema_stack.last["required"] - object_top.last.keys).empty?
|
81
|
+
raise ParserError
|
82
|
+
end
|
83
|
+
|
84
|
+
case stack.last.first
|
85
|
+
when :property
|
86
|
+
schema_stack.pop
|
87
|
+
last = stack.pop
|
88
|
+
matching_type expected_type(schema_stack.last, last), "object", stack.last.first == :object do
|
89
|
+
stack.last.last[last.last] = object_top.last
|
90
|
+
end
|
91
|
+
when ROOT
|
92
|
+
matching_type schema_stack.last["type"], "object" do
|
93
|
+
r = object_top.last
|
94
|
+
end
|
95
|
+
else
|
96
|
+
raise "unknown stack type #{stack.last.first}"
|
97
|
+
end
|
98
|
+
when :integer, :string, :float, :boolean, :null
|
99
|
+
case stack.last.first
|
100
|
+
when :property
|
101
|
+
last = stack.pop
|
102
|
+
matching_type expected_type(schema_stack.last, last), type, stack.last.first == :object do
|
103
|
+
stack.last.last[last.last] = value
|
104
|
+
end
|
105
|
+
when :array
|
106
|
+
case schema_stack.last["items"]
|
107
|
+
when Hash
|
108
|
+
matching_type schema_stack.last["items"]["type"], type do
|
109
|
+
stack.last.last << value
|
110
|
+
end
|
111
|
+
when Array
|
112
|
+
matching_type schema_stack.last["items"][stack.last.last.size]["type"], type do
|
113
|
+
stack.last.last << value
|
114
|
+
end
|
115
|
+
else
|
116
|
+
raise "Unexpected items type #{schema_stack.last["items"]}"
|
117
|
+
end
|
118
|
+
when ROOT
|
119
|
+
matching_type schema_stack.last["type"], type do
|
120
|
+
r = stack.last.last
|
121
|
+
end
|
122
|
+
else
|
123
|
+
raise "unknown stack type #{stack.last.inspect}"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
raise "unhandled token type: #{type}: #{value}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
r
|
130
|
+
end
|
131
|
+
|
132
|
+
def expected_type schema, last
|
133
|
+
schema["properties"][last.last] && schema["properties"][last.last]["type"]
|
134
|
+
end
|
135
|
+
|
136
|
+
def matching_type expected, actual, opt=true
|
137
|
+
if is_type(expected, actual.to_s) && opt
|
138
|
+
yield
|
139
|
+
else
|
140
|
+
raise ParserError, "expected node of type #{expected} but was #{actual}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
TYPE_WIDENINGS = {
|
145
|
+
'integer' => 'number',
|
146
|
+
'float' => 'number'
|
147
|
+
}
|
148
|
+
def is_type expected, actual
|
149
|
+
case expected
|
150
|
+
when String
|
151
|
+
expected == actual || expected == TYPE_WIDENINGS[actual]
|
152
|
+
when Array
|
153
|
+
expected.any? {|e| is_type e, actual }
|
154
|
+
when nil
|
155
|
+
true # is this really what the spec wants? really?
|
156
|
+
else
|
157
|
+
raise "unexpected type comparison #{expected}, #{actual}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class ParserError < StandardError
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
# comes from https://github.com/flori/json/blob/master/lib/json/pure/parser.rb + modifications to make it a lexer
|
2
|
+
# terrible, I know, but this is a hack afterall
|
3
|
+
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
module Muskox
|
7
|
+
module Pure
|
8
|
+
class Lexer < StringScanner
|
9
|
+
STRING = /" ((?:[^\x0-\x1f"\\] |
|
10
|
+
# escaped special characters:
|
11
|
+
\\["\\\/bfnrt] |
|
12
|
+
\\u[0-9a-fA-F]{4} |
|
13
|
+
# match all but escaped special characters:
|
14
|
+
\\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*)
|
15
|
+
"/nx
|
16
|
+
INTEGER = /(-?0|-?[1-9]\d*)/
|
17
|
+
FLOAT = /(-?
|
18
|
+
(?:0|[1-9]\d*)
|
19
|
+
(?:
|
20
|
+
\.\d+(?i:e[+-]?\d+) |
|
21
|
+
\.\d+ |
|
22
|
+
(?i:e[+-]?\d+)
|
23
|
+
)
|
24
|
+
)/x
|
25
|
+
NAN = /NaN/
|
26
|
+
INFINITY = /Infinity/
|
27
|
+
MINUS_INFINITY = /-Infinity/
|
28
|
+
OBJECT_OPEN = /\{/
|
29
|
+
OBJECT_CLOSE = /\}/
|
30
|
+
ARRAY_OPEN = /\[/
|
31
|
+
ARRAY_CLOSE = /\]/
|
32
|
+
PAIR_DELIMITER = /:/
|
33
|
+
COLLECTION_DELIMITER = /,/
|
34
|
+
TRUE = /true/
|
35
|
+
FALSE = /false/
|
36
|
+
NULL = /null/
|
37
|
+
IGNORE = %r(
|
38
|
+
(?:
|
39
|
+
//[^\n\r]*[\n\r]| # line comments
|
40
|
+
/\* # c-style comments
|
41
|
+
(?:
|
42
|
+
[^*/]| # normal chars
|
43
|
+
/[^*]| # slashes that do not start a nested comment
|
44
|
+
\*[^/]| # asterisks that do not end this comment
|
45
|
+
/(?=\*/) # single slash before this comment's end
|
46
|
+
)*
|
47
|
+
\*/ # the End of this comment
|
48
|
+
|[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr
|
49
|
+
)+
|
50
|
+
)mx
|
51
|
+
|
52
|
+
UNPARSED = Object.new
|
53
|
+
|
54
|
+
# Creates a new JSON::Pure::Parser instance for the string _source_.
|
55
|
+
#
|
56
|
+
# It will be configured by the _opts_ hash. _opts_ can have the following
|
57
|
+
# keys:
|
58
|
+
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
|
59
|
+
# structures. Disable depth checking with :max_nesting => false|nil|0,
|
60
|
+
# it defaults to 100.
|
61
|
+
# * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
|
62
|
+
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
|
63
|
+
# to false.
|
64
|
+
# * *symbolize_names*: If set to true, returns symbols for the names
|
65
|
+
# (keys) in a JSON object. Otherwise strings are returned, which is also
|
66
|
+
# the default.
|
67
|
+
# * *quirks_mode*: Enables quirks_mode for parser, that is for example
|
68
|
+
# parsing single JSON values instead of documents is possible.
|
69
|
+
def initialize(source, opts = {})
|
70
|
+
opts ||= {}
|
71
|
+
unless @quirks_mode = opts[:quirks_mode]
|
72
|
+
source = convert_encoding source
|
73
|
+
end
|
74
|
+
super source
|
75
|
+
if !opts.key?(:max_nesting) # defaults to 100
|
76
|
+
@max_nesting = 100
|
77
|
+
elsif opts[:max_nesting]
|
78
|
+
@max_nesting = opts[:max_nesting]
|
79
|
+
else
|
80
|
+
@max_nesting = 0
|
81
|
+
end
|
82
|
+
@allow_nan = !!opts[:allow_nan]
|
83
|
+
@symbolize_names = !!opts[:symbolize_names]
|
84
|
+
@match_string = opts[:match_string]
|
85
|
+
end
|
86
|
+
|
87
|
+
alias source string
|
88
|
+
|
89
|
+
def quirks_mode?
|
90
|
+
!!@quirks_mode
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset
|
94
|
+
super
|
95
|
+
@current_nesting = 0
|
96
|
+
end
|
97
|
+
|
98
|
+
# Parses the current JSON string _source_ and returns the complete data
|
99
|
+
# structure as a result.
|
100
|
+
def lex &block
|
101
|
+
@callback = block
|
102
|
+
reset
|
103
|
+
if @quirks_mode
|
104
|
+
while !eos? && skip(IGNORE)
|
105
|
+
end
|
106
|
+
if eos?
|
107
|
+
raise ParserError, "source did not contain any JSON!"
|
108
|
+
else
|
109
|
+
obj = lex_value
|
110
|
+
obj == UNPARSED and raise ParserError, "source did not contain any JSON!"
|
111
|
+
end
|
112
|
+
else
|
113
|
+
until eos?
|
114
|
+
case
|
115
|
+
when scan(OBJECT_OPEN)
|
116
|
+
# obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
|
117
|
+
@current_nesting = 1
|
118
|
+
lex_object
|
119
|
+
when scan(ARRAY_OPEN)
|
120
|
+
# obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
|
121
|
+
@current_nesting = 1
|
122
|
+
lex_array
|
123
|
+
when skip(IGNORE)
|
124
|
+
;
|
125
|
+
else
|
126
|
+
raise ParserError, "source '#{peek(20)}' not in JSON!"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# obj or raise ParserError, "source did not contain any JSON!"
|
130
|
+
end
|
131
|
+
# obj
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def convert_encoding(source)
|
137
|
+
if source.respond_to?(:to_str)
|
138
|
+
source = source.to_str
|
139
|
+
else
|
140
|
+
raise TypeError, "#{source.inspect} is not like a string"
|
141
|
+
end
|
142
|
+
if defined?(::Encoding)
|
143
|
+
if source.encoding == ::Encoding::ASCII_8BIT
|
144
|
+
b = source[0, 4].bytes.to_a
|
145
|
+
source =
|
146
|
+
case
|
147
|
+
when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
|
148
|
+
source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8)
|
149
|
+
when b.size >= 4 && b[0] == 0 && b[2] == 0
|
150
|
+
source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8)
|
151
|
+
when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
|
152
|
+
source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8)
|
153
|
+
when b.size >= 4 && b[1] == 0 && b[3] == 0
|
154
|
+
source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8)
|
155
|
+
else
|
156
|
+
source.dup
|
157
|
+
end
|
158
|
+
else
|
159
|
+
source = source.encode(::Encoding::UTF_8)
|
160
|
+
end
|
161
|
+
source.force_encoding(::Encoding::ASCII_8BIT)
|
162
|
+
else
|
163
|
+
b = source
|
164
|
+
source =
|
165
|
+
case
|
166
|
+
when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
|
167
|
+
JSON.iconv('utf-8', 'utf-32be', b)
|
168
|
+
when b.size >= 4 && b[0] == 0 && b[2] == 0
|
169
|
+
JSON.iconv('utf-8', 'utf-16be', b)
|
170
|
+
when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
|
171
|
+
JSON.iconv('utf-8', 'utf-32le', b)
|
172
|
+
when b.size >= 4 && b[1] == 0 && b[3] == 0
|
173
|
+
JSON.iconv('utf-8', 'utf-16le', b)
|
174
|
+
else
|
175
|
+
b
|
176
|
+
end
|
177
|
+
end
|
178
|
+
source
|
179
|
+
end
|
180
|
+
|
181
|
+
# Unescape characters in strings.
|
182
|
+
UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
|
183
|
+
UNESCAPE_MAP.update({
|
184
|
+
?" => '"',
|
185
|
+
?\\ => '\\',
|
186
|
+
?/ => '/',
|
187
|
+
?b => "\b",
|
188
|
+
?f => "\f",
|
189
|
+
?n => "\n",
|
190
|
+
?r => "\r",
|
191
|
+
?t => "\t",
|
192
|
+
?u => nil,
|
193
|
+
})
|
194
|
+
|
195
|
+
EMPTY_8BIT_STRING = ''
|
196
|
+
if ::String.method_defined?(:encode)
|
197
|
+
EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
|
198
|
+
end
|
199
|
+
|
200
|
+
def parse_string
|
201
|
+
if scan(STRING)
|
202
|
+
return '' if self[1].empty?
|
203
|
+
string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c|
|
204
|
+
if u = UNESCAPE_MAP[$&[1]]
|
205
|
+
u
|
206
|
+
else # \uXXXX
|
207
|
+
bytes = EMPTY_8BIT_STRING.dup
|
208
|
+
i = 0
|
209
|
+
while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
|
210
|
+
bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
|
211
|
+
i += 1
|
212
|
+
end
|
213
|
+
JSON.iconv('utf-8', 'utf-16be', bytes)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
if string.respond_to?(:force_encoding)
|
217
|
+
string.force_encoding(::Encoding::UTF_8)
|
218
|
+
end
|
219
|
+
string
|
220
|
+
else
|
221
|
+
UNPARSED
|
222
|
+
end
|
223
|
+
rescue => e
|
224
|
+
raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def lex_value
|
228
|
+
case
|
229
|
+
when scan(FLOAT)
|
230
|
+
@callback.call :float, Float(self[1])
|
231
|
+
when scan(INTEGER)
|
232
|
+
@callback.call :integer, Integer(self[1])
|
233
|
+
when scan(TRUE)
|
234
|
+
@callback.call :boolean, true
|
235
|
+
when scan(FALSE)
|
236
|
+
@callback.call :boolean, false
|
237
|
+
when scan(NULL)
|
238
|
+
@callback.call :null, nil
|
239
|
+
when (string = parse_string) != UNPARSED
|
240
|
+
@callback.call :string, string
|
241
|
+
when scan(ARRAY_OPEN)
|
242
|
+
@current_nesting += 1
|
243
|
+
lex_array
|
244
|
+
@current_nesting -= 1
|
245
|
+
when scan(OBJECT_OPEN)
|
246
|
+
@current_nesting += 1
|
247
|
+
lex_object
|
248
|
+
@current_nesting -= 1
|
249
|
+
# when @allow_nan && scan(NAN)
|
250
|
+
# NaN
|
251
|
+
# when @allow_nan && scan(INFINITY)
|
252
|
+
# Infinity
|
253
|
+
# when @allow_nan && scan(MINUS_INFINITY)
|
254
|
+
# MinusInfinity
|
255
|
+
else
|
256
|
+
UNPARSED
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def lex_array
|
261
|
+
raise NestingError, "nesting of #@current_nesting is too deep" if
|
262
|
+
@max_nesting.nonzero? && @current_nesting > @max_nesting
|
263
|
+
@callback.call :array_begin, nil
|
264
|
+
delim = false
|
265
|
+
until eos?
|
266
|
+
case
|
267
|
+
when (value = lex_value) != UNPARSED
|
268
|
+
delim = false
|
269
|
+
|
270
|
+
skip(IGNORE)
|
271
|
+
if scan(COLLECTION_DELIMITER)
|
272
|
+
delim = true
|
273
|
+
elsif match?(ARRAY_CLOSE)
|
274
|
+
;
|
275
|
+
else
|
276
|
+
raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
|
277
|
+
end
|
278
|
+
when scan(ARRAY_CLOSE)
|
279
|
+
if delim
|
280
|
+
raise ParserError, "expected next element in array at '#{peek(20)}'!"
|
281
|
+
end
|
282
|
+
break
|
283
|
+
when skip(IGNORE)
|
284
|
+
;
|
285
|
+
else
|
286
|
+
raise ParserError, "unexpected token in array at '#{peek(20)}'!"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
@callback.call :array_end, nil
|
290
|
+
end
|
291
|
+
|
292
|
+
def lex_object
|
293
|
+
raise NestingError, "nesting of #@current_nesting is too deep" if
|
294
|
+
@max_nesting.nonzero? && @current_nesting > @max_nesting
|
295
|
+
|
296
|
+
|
297
|
+
@callback.call :object_begin, nil
|
298
|
+
delim = false
|
299
|
+
until eos?
|
300
|
+
case
|
301
|
+
when (string = parse_string) != UNPARSED
|
302
|
+
@callback.call :property, string
|
303
|
+
skip(IGNORE)
|
304
|
+
unless scan(PAIR_DELIMITER)
|
305
|
+
raise ParserError, "expected ':' in object at '#{peek(20)}'!"
|
306
|
+
end
|
307
|
+
skip(IGNORE)
|
308
|
+
unless (value = lex_value).equal? UNPARSED
|
309
|
+
delim = false
|
310
|
+
skip(IGNORE)
|
311
|
+
if scan(COLLECTION_DELIMITER)
|
312
|
+
delim = true
|
313
|
+
elsif match?(OBJECT_CLOSE)
|
314
|
+
;
|
315
|
+
else
|
316
|
+
raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
|
317
|
+
end
|
318
|
+
else
|
319
|
+
raise ParserError, "expected value in object at '#{peek(20)}'!"
|
320
|
+
end
|
321
|
+
when scan(OBJECT_CLOSE)
|
322
|
+
if delim
|
323
|
+
raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
|
324
|
+
end
|
325
|
+
break
|
326
|
+
when skip(IGNORE)
|
327
|
+
;
|
328
|
+
else
|
329
|
+
raise ParserError, "unexpected token in object at '#{peek(20)}'!"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
@callback.call :object_end, nil
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
data/muskox.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'muskox/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "muskox"
|
8
|
+
spec.version = Muskox::VERSION
|
9
|
+
spec.authors = ["Nick Howard"]
|
10
|
+
spec.email = ["ndh@baroquebobcat.com"]
|
11
|
+
spec.description = %q{A JSON-Schema based Parser-Generator}
|
12
|
+
spec.summary = %q{A JSON-Schema based Parser-Generator}
|
13
|
+
spec.homepage = "https://github.com/baroquebobcat/muskox"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'muskox'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
|
6
|
+
to_skip = [
|
7
|
+
# Allows wrong type sometimes for some reason
|
8
|
+
["ignores non-arrays", "draft4/items.json", "a schema given for items"]
|
9
|
+
]
|
10
|
+
|
11
|
+
['draft4/items.json', 'draft4/type.json'].each do |file|
|
12
|
+
json = JSON.parse(open("json_schema_test_suite/tests/#{file}").read)
|
13
|
+
describe file do
|
14
|
+
json.each do |t|
|
15
|
+
describe t["description"] do
|
16
|
+
before do
|
17
|
+
schema = t["schema"]
|
18
|
+
@parser = Muskox.generate schema
|
19
|
+
end
|
20
|
+
t["tests"].each do |test|
|
21
|
+
it test["description"] do
|
22
|
+
skip if to_skip.include? [test["description"], file, t["description"]]
|
23
|
+
if test["valid"]
|
24
|
+
@parser.parse test["data"].to_json
|
25
|
+
else
|
26
|
+
assert_raises Muskox::ParserError do
|
27
|
+
@parser.parse test["data"].to_json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/spec/muskox_spec.rb
ADDED
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'muskox'
|
3
|
+
|
4
|
+
describe Muskox do
|
5
|
+
describe "simple object[number]=integer schema, error on extra property" do
|
6
|
+
before do
|
7
|
+
schema = {
|
8
|
+
"title" => "Schema",
|
9
|
+
"type" => "object",
|
10
|
+
"properties" => {
|
11
|
+
"number" => {
|
12
|
+
"type" => "integer"
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"required" => ["number"]
|
16
|
+
}
|
17
|
+
|
18
|
+
@parser = Muskox.generate schema
|
19
|
+
end
|
20
|
+
it "parses successfully when passed a valid string" do
|
21
|
+
result = @parser.parse %!{"number": 1}!
|
22
|
+
assert_equal({"number" => 1 }, result)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "parses successfully when passed a different valid string" do
|
26
|
+
result = @parser.parse %!{"number": 2}!
|
27
|
+
assert_equal({"number" => 2 }, result)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "raises an error when there is an extra property" do
|
31
|
+
assert_raises Muskox::ParserError do
|
32
|
+
result = @parser.parse %!{"number": 2, "grug":[]}!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
it "raises an error when there is an invalid type of property" do
|
36
|
+
assert_raises Muskox::ParserError do
|
37
|
+
result = @parser.parse %!{"number": "string-not-number"}!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
it "raises an error when there is a missing property" do
|
41
|
+
assert_raises Muskox::ParserError do
|
42
|
+
result = @parser.parse %!{}!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
describe "simple object[string]=string schema, error on extra property" do
|
50
|
+
before do
|
51
|
+
schema = {
|
52
|
+
"title" => "Schema",
|
53
|
+
"type" => "object",
|
54
|
+
"properties" => {
|
55
|
+
"string" => {
|
56
|
+
"type" => "string"
|
57
|
+
}
|
58
|
+
},
|
59
|
+
"required" => ["string"]
|
60
|
+
}
|
61
|
+
|
62
|
+
@parser = Muskox.generate schema
|
63
|
+
end
|
64
|
+
it "parses successfully when passed a valid string" do
|
65
|
+
result = @parser.parse %!{"string": "one"}!
|
66
|
+
assert_equal({"string" => "one" }, result)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "parses successfully when passed a different valid string" do
|
70
|
+
result = @parser.parse %!{"string": "two"}!
|
71
|
+
assert_equal({"string" => "two" }, result)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "raises an error when there is an extra property" do
|
75
|
+
assert_raises Muskox::ParserError do
|
76
|
+
result = @parser.parse %!{"string": "two", "grug":[]}!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
it "raises an error when there is an invalid type of property" do
|
80
|
+
assert_raises Muskox::ParserError do
|
81
|
+
result = @parser.parse %!{"string": 1701}!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe " object[array]=array[string] schema, error on extra property" do
|
87
|
+
before do
|
88
|
+
schema = {
|
89
|
+
"title" => "Schema",
|
90
|
+
"type" => "object",
|
91
|
+
"properties" => {
|
92
|
+
"array" => {
|
93
|
+
"type" => "array",
|
94
|
+
"items" => {"type" => "string"}
|
95
|
+
}
|
96
|
+
},
|
97
|
+
"required" => ["array"]
|
98
|
+
}
|
99
|
+
|
100
|
+
@parser = Muskox.generate schema
|
101
|
+
end
|
102
|
+
it "parses successfully when passed a valid string" do
|
103
|
+
result = @parser.parse %!{"array": ["one"]}!
|
104
|
+
assert_equal({"array" => ["one"] }, result)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "parses successfully when passed a different valid array" do
|
108
|
+
result = @parser.parse %!{"array": ["two"]}!
|
109
|
+
assert_equal({"array" => ["two"] }, result)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "parses successfully when passed a valid array of size 2" do
|
113
|
+
result = @parser.parse %!{"array": ["two", "one"]}!
|
114
|
+
assert_equal({"array" => ["two", "one"] }, result)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "raises an error when there is an extra property" do
|
118
|
+
assert_raises Muskox::ParserError do
|
119
|
+
result = @parser.parse %!{"array": ["two"], "grug":[]}!
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it "raises an error when there is an invalid component type of property" do
|
124
|
+
assert_raises Muskox::ParserError do
|
125
|
+
result = @parser.parse %!{"array": [1701]}!
|
126
|
+
p result
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
describe "object[object]=object schema, error on extra property" do
|
133
|
+
before do
|
134
|
+
schema = {
|
135
|
+
"title" => "Schema",
|
136
|
+
"type" => "object",
|
137
|
+
"properties" => {
|
138
|
+
"object" => {
|
139
|
+
"type" => "object"
|
140
|
+
}
|
141
|
+
},
|
142
|
+
"required" => ["object"]
|
143
|
+
}
|
144
|
+
|
145
|
+
@parser = Muskox.generate schema
|
146
|
+
end
|
147
|
+
it "parses successfully when passed a valid string" do
|
148
|
+
result = @parser.parse %!{"object": {}}!
|
149
|
+
assert_equal({"object" => {} }, result)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "parses successfully when passed a different valid string" do
|
153
|
+
result = @parser.parse %!{"object": {}}!
|
154
|
+
assert_equal({"object" => {} }, result)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "raises an error when there is an extra property" do
|
158
|
+
assert_raises Muskox::ParserError do
|
159
|
+
result = @parser.parse %!{"object": {}, "grug":[]}!
|
160
|
+
end
|
161
|
+
end
|
162
|
+
it "raises an error when there is an invalid type of property" do
|
163
|
+
assert_raises Muskox::ParserError do
|
164
|
+
result = @parser.parse %!{"object": "string"}!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "object[object]=object[string]=string schema, error on extra property" do
|
170
|
+
before do
|
171
|
+
schema = {
|
172
|
+
"title" => "Schema",
|
173
|
+
"type" => "object",
|
174
|
+
"properties" => {
|
175
|
+
"object" => {
|
176
|
+
"type" => "object",
|
177
|
+
"properties" => {"string" => {"type" => "string"}},
|
178
|
+
"required" => ["string"]
|
179
|
+
}
|
180
|
+
},
|
181
|
+
"required" => ["object"]
|
182
|
+
}
|
183
|
+
|
184
|
+
@parser = Muskox.generate schema
|
185
|
+
end
|
186
|
+
it "parses successfully when passed a valid string" do
|
187
|
+
result = @parser.parse %!{"object": {"string":"a"}}!
|
188
|
+
assert_equal({"object" => {"string" => "a"} }, result)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "parses successfully when passed a different valid string" do
|
192
|
+
result = @parser.parse %!{"object": {"string":"b"}}!
|
193
|
+
assert_equal({"object" => {"string" => "b"} }, result)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "raises an error when there is an extra nested property" do
|
197
|
+
assert_raises Muskox::ParserError do
|
198
|
+
result = @parser.parse %!{"object": {"string":"a","grug":[]}, }!
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
it "raises an error when there is an invalid type of nested property" do
|
203
|
+
assert_raises Muskox::ParserError do
|
204
|
+
result = @parser.parse %!{"object": {"string":1}}!
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe "simple object[number]=float happy path" do
|
210
|
+
before do
|
211
|
+
schema = {
|
212
|
+
"title" => "Schema",
|
213
|
+
"type" => "object",
|
214
|
+
"properties" => {
|
215
|
+
"number" => {
|
216
|
+
"type" => "float"
|
217
|
+
}
|
218
|
+
},
|
219
|
+
"required" => ["number"]
|
220
|
+
}
|
221
|
+
|
222
|
+
@parser = Muskox.generate schema
|
223
|
+
end
|
224
|
+
it "parses successfully when passed a valid string" do
|
225
|
+
result = @parser.parse %!{"number": 1.0}!
|
226
|
+
assert_equal({"number" => 1.0 }, result)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
describe "simple object[number]=boolean happy path" do
|
232
|
+
before do
|
233
|
+
schema = {
|
234
|
+
"title" => "Schema",
|
235
|
+
"type" => "object",
|
236
|
+
"properties" => {
|
237
|
+
"number" => {
|
238
|
+
"type" => "boolean"
|
239
|
+
}
|
240
|
+
},
|
241
|
+
"required" => ["number"]
|
242
|
+
}
|
243
|
+
|
244
|
+
@parser = Muskox.generate schema
|
245
|
+
end
|
246
|
+
it "parses successfully when passed a valid string" do
|
247
|
+
result = @parser.parse %!{"number": true}!
|
248
|
+
assert_equal({"number" => true }, result)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "malformed json handling" do
|
253
|
+
before do
|
254
|
+
schema = {
|
255
|
+
"type" => "object",
|
256
|
+
"properties" => {
|
257
|
+
"number" => {
|
258
|
+
"type" => "boolean"
|
259
|
+
}
|
260
|
+
},
|
261
|
+
"required" => ["number"]
|
262
|
+
}
|
263
|
+
|
264
|
+
@parser = Muskox.generate schema
|
265
|
+
end
|
266
|
+
it "raises an error when object unended" do
|
267
|
+
assert_raises Muskox::ParserError do
|
268
|
+
result = @parser.parse %!{"number": true!
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
#null
|
274
|
+
#array size limits
|
275
|
+
# bad JSON strings
|
276
|
+
|
277
|
+
end
|
278
|
+
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: muskox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Howard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: A JSON-Schema based Parser-Generator
|
42
|
+
email:
|
43
|
+
- ndh@baroquebobcat.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- .gitmodules
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- lib/muskox.rb
|
55
|
+
- lib/muskox/json_lexer.rb
|
56
|
+
- lib/muskox/version.rb
|
57
|
+
- muskox.gemspec
|
58
|
+
- spec/json_schema_spec.rb
|
59
|
+
- spec/muskox_spec.rb
|
60
|
+
homepage: https://github.com/baroquebobcat/muskox
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 2.0.3
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: A JSON-Schema based Parser-Generator
|
84
|
+
test_files:
|
85
|
+
- spec/json_schema_spec.rb
|
86
|
+
- spec/muskox_spec.rb
|