yes 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby +41 -0
- data/.yardopts +8 -0
- data/HISTORY.rdoc +31 -0
- data/LICENSE.rdoc +35 -0
- data/QED.rdoc +1036 -0
- data/README.rdoc +144 -0
- data/THANKS.rdoc +10 -0
- data/bin/yes-lint +4 -0
- data/data/yes/yes.yes +21 -0
- data/lib/yes.rb +38 -0
- data/lib/yes/cli.rb +20 -0
- data/lib/yes/constraints/abstract_constraint.rb +121 -0
- data/lib/yes/constraints/choice.rb +39 -0
- data/lib/yes/constraints/count.rb +38 -0
- data/lib/yes/constraints/exclusive.rb +48 -0
- data/lib/yes/constraints/fnmatch.rb +42 -0
- data/lib/yes/constraints/inclusive.rb +50 -0
- data/lib/yes/constraints/key.rb +55 -0
- data/lib/yes/constraints/kind.rb +34 -0
- data/lib/yes/constraints/length.rb +46 -0
- data/lib/yes/constraints/node_constraint.rb +55 -0
- data/lib/yes/constraints/range.rb +43 -0
- data/lib/yes/constraints/regexp.rb +52 -0
- data/lib/yes/constraints/required.rb +45 -0
- data/lib/yes/constraints/requires.rb +57 -0
- data/lib/yes/constraints/tag.rb +55 -0
- data/lib/yes/constraints/tree_constraint.rb +14 -0
- data/lib/yes/constraints/type.rb +91 -0
- data/lib/yes/constraints/value.rb +62 -0
- data/lib/yes/genclass.rb +2 -0
- data/lib/yes/lint.rb +101 -0
- data/lib/yes/logical_and.rb +13 -0
- metadata +110 -0
data/README.rdoc
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
= YES - YAML Easy Schema
|
2
|
+
|
3
|
+
Author:: Thomas Sawyer
|
4
|
+
License:: BSD-2-Clause
|
5
|
+
Copyright:: (c) 2011 Thomas Sawyer, Rubyworks
|
6
|
+
|
7
|
+
|
8
|
+
== SALES PITCH
|
9
|
+
|
10
|
+
It doesn't get any easier than this!
|
11
|
+
|
12
|
+
YES is a schema system for YAML that is intuitive and extremely powerful.
|
13
|
+
|
14
|
+
YES Schemas are also YAML documents, so they eat there own dog food.
|
15
|
+
|
16
|
+
|
17
|
+
== HOW IT WORKS
|
18
|
+
|
19
|
+
The design of YeS is as simple as it is powerful. A YeS schema is composed
|
20
|
+
of YPath selectors mapped to document constraints. YPath is a syntax
|
21
|
+
for selecting *nodes* from a YAML document.
|
22
|
+
|
23
|
+
When validating a YAML document against a YeS schema a "lint" program
|
24
|
+
simply collects all matching nodes with their applicable constraints into
|
25
|
+
a collection of *unit-validations*. Then this collection is filtered of
|
26
|
+
all *passing* validations. All that is left are the failures. If the filtered
|
27
|
+
list is empty the document is completely valid. If not empty, the lint
|
28
|
+
program can provide a detailed *editorial* list of the failures.
|
29
|
+
|
30
|
+
Constraints are given as a mappings of validation type to validation
|
31
|
+
parameters.
|
32
|
+
|
33
|
+
Lets take an example:
|
34
|
+
|
35
|
+
people/*/name:
|
36
|
+
regexp: '[^/n]'
|
37
|
+
|
38
|
+
This simple schema selects all nodes under a `people` sequence of
|
39
|
+
mappings with a name key, the value of which cannot contain newlines.
|
40
|
+
In other words:
|
41
|
+
|
42
|
+
--
|
43
|
+
people:
|
44
|
+
- name: Charlie Adams
|
45
|
+
- name: Julie Ann Rose
|
46
|
+
|
47
|
+
Would satisfy the schema. But,
|
48
|
+
|
49
|
+
--
|
50
|
+
people:
|
51
|
+
- name: |
|
52
|
+
Charlie
|
53
|
+
Adams
|
54
|
+
|
55
|
+
Would not.
|
56
|
+
|
57
|
+
TODO: Better Examples!!!
|
58
|
+
|
59
|
+
Sometimes multiple constraints of the same type need to be applied to
|
60
|
+
a set of nodes. This can be done by expressing the same YPath with
|
61
|
+
different constraints, for example:
|
62
|
+
|
63
|
+
people/*/name:
|
64
|
+
regexp: '[^/t]'
|
65
|
+
people/*/name:
|
66
|
+
regexp: '[^/n]'
|
67
|
+
|
68
|
+
But to make the intent more succinct a sequence of constraints can be give
|
69
|
+
along with the *logical-and* tag , `!!and`.
|
70
|
+
|
71
|
+
people/*/name: !!and
|
72
|
+
- regexp: '[^/t]'
|
73
|
+
- regexp: '[^/n]'
|
74
|
+
|
75
|
+
If the `!!and` tag is not given, then the default operator is used,
|
76
|
+
*logical-or*, which can also be explicitly stated as `!!or`:
|
77
|
+
|
78
|
+
people/*/name: !!or
|
79
|
+
- regexp: '[^/t]'
|
80
|
+
- regexp: '[^/n]'
|
81
|
+
|
82
|
+
In this way logical relationships of constraints can be created.
|
83
|
+
|
84
|
+
people/*/name: !!or
|
85
|
+
- !!and
|
86
|
+
- regexp: '[^/t]'
|
87
|
+
- regexp: '[^/n]'
|
88
|
+
- !!and
|
89
|
+
- regexp: '[^/t]'
|
90
|
+
- regexp: '[^/n]'
|
91
|
+
|
92
|
+
|
93
|
+
== COMMAND LINE
|
94
|
+
|
95
|
+
To use on the command line *lint* tool. Say we have a `schema.yes` file:
|
96
|
+
|
97
|
+
---
|
98
|
+
name:
|
99
|
+
type: str
|
100
|
+
regexp: '[^\n]'
|
101
|
+
required: true
|
102
|
+
age:
|
103
|
+
type: int
|
104
|
+
birth:
|
105
|
+
type: date
|
106
|
+
|
107
|
+
Try it on `sample.yaml`.
|
108
|
+
|
109
|
+
---
|
110
|
+
name: Thomas T. Thomas
|
111
|
+
age: 42
|
112
|
+
birth: 1976-07-04
|
113
|
+
|
114
|
+
Using the executable.
|
115
|
+
|
116
|
+
$ yes-lint schema.yes sample.yaml
|
117
|
+
|
118
|
+
In code that is:
|
119
|
+
|
120
|
+
require 'yes'
|
121
|
+
|
122
|
+
lint = YES::Lint.new(File.new('schema.yes'))
|
123
|
+
|
124
|
+
lint.validate(File.new('sample.yaml'))
|
125
|
+
|
126
|
+
|
127
|
+
== CONTRIBUTE
|
128
|
+
|
129
|
+
Come on folks! Let's get YAML up to snuff. A good Schema could really
|
130
|
+
help YAML go the distance and penetrate some of those "Enterprisey" worlds.
|
131
|
+
|
132
|
+
* Please read, write and comment on issues[https://github.com/rubyworks/yes/issues].
|
133
|
+
* Please critique the code.
|
134
|
+
* Please fork and submit patches in topic branches.
|
135
|
+
|
136
|
+
And please contribute to {Rubyworks Ruby Development Fund}[http://rubyworks.github.org]
|
137
|
+
so us poor Ruby OSS developers can eat :)
|
138
|
+
|
139
|
+
|
140
|
+
== COPYRIGHT
|
141
|
+
|
142
|
+
(BSD-2 license)
|
143
|
+
|
144
|
+
Copyright (c) 2011 Rubyworks, Thomas Sawyer
|
data/THANKS.rdoc
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
= SPECIAL THANKS
|
2
|
+
|
3
|
+
== Kwaify
|
4
|
+
|
5
|
+
Copyright:: (c) 2005-2010 Kuwata Lab. All rights reserved.
|
6
|
+
License:: MIT
|
7
|
+
Website:: http://www.kuwata-lab.com/kwalify
|
8
|
+
|
9
|
+
Although no Kwalify code was actually used to build YES, it provided
|
10
|
+
inspiration for some of the design choices.
|
data/bin/yes-lint
ADDED
data/data/yes/yes.yes
ADDED
data/lib/yes.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module YES
|
2
|
+
|
3
|
+
# Simple validatily check.
|
4
|
+
#
|
5
|
+
# @return [Boolean]
|
6
|
+
def self.valid?(schema, yaml)
|
7
|
+
yes = YES::Lint.new(@schema)
|
8
|
+
yes.validate(@yaml).empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
require 'yaml'
|
15
|
+
|
16
|
+
require 'yes/constraints/abstract_constraint'
|
17
|
+
require 'yes/constraints/node_constraint'
|
18
|
+
require 'yes/constraints/tree_constraint'
|
19
|
+
|
20
|
+
require 'yes/constraints/range'
|
21
|
+
require 'yes/constraints/regexp'
|
22
|
+
require 'yes/constraints/fnmatch'
|
23
|
+
|
24
|
+
require 'yes/constraints/tag'
|
25
|
+
require 'yes/constraints/type'
|
26
|
+
|
27
|
+
require 'yes/constraints/count'
|
28
|
+
require 'yes/constraints/length'
|
29
|
+
require 'yes/constraints/requires'
|
30
|
+
require 'yes/constraints/inclusive'
|
31
|
+
require 'yes/constraints/exclusive'
|
32
|
+
|
33
|
+
require 'yes/constraints/value'
|
34
|
+
require 'yes/constraints/key'
|
35
|
+
|
36
|
+
require 'yes/logical_and'
|
37
|
+
require 'yes/lint'
|
38
|
+
|
data/lib/yes/cli.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'yes'
|
2
|
+
|
3
|
+
module YES
|
4
|
+
|
5
|
+
def self.cli(*argv)
|
6
|
+
schema_file = argv[0]
|
7
|
+
target_file = argv[1]
|
8
|
+
|
9
|
+
lint = Lint.new(File.new(schema_file))
|
10
|
+
edit = lint.validate(File.new(target_file))
|
11
|
+
|
12
|
+
if edit.size == 0
|
13
|
+
#$stderr.puts "valid: #{target_file}"
|
14
|
+
else
|
15
|
+
$stderr.puts edit.to_yaml
|
16
|
+
exit -1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module YES
|
2
|
+
|
3
|
+
#
|
4
|
+
def self.constraints
|
5
|
+
@constraints ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
#
|
9
|
+
module Constraints
|
10
|
+
|
11
|
+
# AbstractConstraint serves as the base class for
|
12
|
+
# all other constraints.
|
13
|
+
class AbstractConstraint
|
14
|
+
|
15
|
+
#
|
16
|
+
def self.inherited(base)
|
17
|
+
YES.constraints << base
|
18
|
+
end
|
19
|
+
|
20
|
+
# noop
|
21
|
+
def self.checklist(spec, tree, nodes)
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
def initialize(spec, tree, nodes)
|
27
|
+
@spec = spec
|
28
|
+
@tree = tree
|
29
|
+
@nodes = nodes
|
30
|
+
end
|
31
|
+
|
32
|
+
public
|
33
|
+
|
34
|
+
#
|
35
|
+
attr :nodes
|
36
|
+
|
37
|
+
#
|
38
|
+
attr :spec
|
39
|
+
|
40
|
+
#
|
41
|
+
attr :tree
|
42
|
+
|
43
|
+
# MUST OVERRIDE THIS METHOD IN SUBCLASSES
|
44
|
+
#
|
45
|
+
def validate(spec)
|
46
|
+
raise "undefined method -- `validate'"
|
47
|
+
end
|
48
|
+
|
49
|
+
# MUST OVERRIDE THIS METHOD IN SUBCLASSES
|
50
|
+
#
|
51
|
+
def self.applicable?(spec)
|
52
|
+
raise "undefined class method -- `applicable?'"
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
def valid?
|
57
|
+
return true unless applicable?
|
58
|
+
recurse_valid?(spec)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
def applicable?
|
63
|
+
self.class.applicable?(spec)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
#
|
69
|
+
def recurse_valid?(spec=nil)
|
70
|
+
spec ||= self.spec
|
71
|
+
case spec
|
72
|
+
when Array # logical-or
|
73
|
+
spec.any?{ |sub_spec| recurse_valid?(sub_spec) }
|
74
|
+
else
|
75
|
+
validate(spec)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Range matching is used by a couple of validators.
|
80
|
+
#
|
81
|
+
# @param range [Array, String]
|
82
|
+
# The range representation.
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# 1..1 =~ 1
|
86
|
+
# 1...2 =~ 1
|
87
|
+
# [1,1] =~ 1
|
88
|
+
# [1,2) =~ 1
|
89
|
+
# (1,2] =~ 2
|
90
|
+
# (1,3) =~ 2
|
91
|
+
#
|
92
|
+
def match_delta(range, value)
|
93
|
+
case range
|
94
|
+
when Array # array can do string comparisons
|
95
|
+
value > range.first && value < range.last
|
96
|
+
when /^(.*)\.\.\.(n|N)$/
|
97
|
+
value >= $1.to_f
|
98
|
+
when /^(.*)\.\.(n|N)$/
|
99
|
+
value >= $1.to_f
|
100
|
+
when /^(.*)\.\.\.(.*)$/
|
101
|
+
value >= $1.to_f && value < $2.to_f
|
102
|
+
when /^(.*)\.\.(.*)$/
|
103
|
+
value >= $1.to_f && value <= $2.to_f
|
104
|
+
when /^\[(.*)\,(.*)\]$/
|
105
|
+
value >= $1.to_f && value <= $2.to_f
|
106
|
+
when /^\[(.*)\,(.*)\)$/
|
107
|
+
value >= $1.to_f && value < $2.to_f
|
108
|
+
when /^\((.*)\,(.*)\]$/
|
109
|
+
value > $1.to_f && value <= $2.to_f
|
110
|
+
when /^\((.*)\,(.*)\)$/
|
111
|
+
value > $1.to_f && value < $2.to_f
|
112
|
+
else # assume range is just a number
|
113
|
+
range.to_f == value.to_f
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module YES
|
2
|
+
|
3
|
+
module Constraints
|
4
|
+
|
5
|
+
# Validate from a selction of choices.
|
6
|
+
#
|
7
|
+
# choice: [M,F]
|
8
|
+
#
|
9
|
+
class Choice < NodeConstraint
|
10
|
+
|
11
|
+
#
|
12
|
+
# @return [Array<Constraint>]
|
13
|
+
def self.checklist(spec, tree, nodes)
|
14
|
+
return [] unless applicable?(spec)
|
15
|
+
nodes.map do |node|
|
16
|
+
new(spec, tree, node)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Only applicable if `choice` field is in spec.
|
21
|
+
def self.applicable?(spec)
|
22
|
+
spec['choice']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Validate that a node's value is amoung a provided
|
26
|
+
# list of values.
|
27
|
+
#
|
28
|
+
# @return [Boolean] validity
|
29
|
+
def validate(spec)
|
30
|
+
choice = Array(spec['choice'])
|
31
|
+
choice.include?(node.transform)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module YES
|
2
|
+
|
3
|
+
module Constraints
|
4
|
+
|
5
|
+
# Count validation ensures there are a limited number of matching
|
6
|
+
# nodes in a document.
|
7
|
+
#
|
8
|
+
# //gold:
|
9
|
+
# count: 1..4
|
10
|
+
#
|
11
|
+
# Would mean the `gold` mapping key could only appear 1 to 4 times
|
12
|
+
# in the entire document.
|
13
|
+
class Count < TreeConstraint
|
14
|
+
|
15
|
+
#
|
16
|
+
# @return [Array<Constraint>]
|
17
|
+
def self.checklist(spec, tree, nodes)
|
18
|
+
return [] unless applicable?(spec)
|
19
|
+
[new(spec, tree, nodes)]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Only applicable if `count` entry is in the spec.
|
23
|
+
def self.applicable?(spec)
|
24
|
+
spec['count']
|
25
|
+
end
|
26
|
+
|
27
|
+
# Validate count ensure there is a minimum and/or maximum
|
28
|
+
# number of matching nodes.
|
29
|
+
def validate(spec)
|
30
|
+
count = spec['count']
|
31
|
+
match_delta(count, nodes.size)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module YES
|
2
|
+
|
3
|
+
module Constraints
|
4
|
+
|
5
|
+
# Exclusion - This can either be a boolean expression in
|
6
|
+
# which case it validates that there is no more than one matching
|
7
|
+
# node. Otherwise, the value is taken to be a ypath and validates
|
8
|
+
# that there are no matching paths if the main selection is present.
|
9
|
+
#--
|
10
|
+
# TODO: Provide $parent$ path substitution ?
|
11
|
+
#++
|
12
|
+
class Exclusive < TreeConstraint
|
13
|
+
|
14
|
+
#
|
15
|
+
# @return [Array<Constraint>]
|
16
|
+
def self.checklist(spec, tree, nodes)
|
17
|
+
return [] unless applicable?(spec)
|
18
|
+
[new(spec, tree, nodes)]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Only applicable if `exclusive` feild is in the spec.
|
22
|
+
def self.applicable?(spec)
|
23
|
+
spec['exclusive']
|
24
|
+
end
|
25
|
+
|
26
|
+
# Exclusion - This can either be a boolean expression in
|
27
|
+
# which case it validates that there is no more than one matching
|
28
|
+
# node. Otherwise, the value is taken to be a ypath and validates
|
29
|
+
# that there are no matching paths if the main selection is present.
|
30
|
+
#
|
31
|
+
# @return [Boolean] validity
|
32
|
+
def validate(spec)
|
33
|
+
exclusive = spec['exclusive']
|
34
|
+
|
35
|
+
case exclusive
|
36
|
+
when true, false
|
37
|
+
nodes.size <= 1
|
38
|
+
else
|
39
|
+
ex_nodes = tree.select(exclusive)
|
40
|
+
nodes.size == 0 or ex_nodes.size == 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|