yes 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.
- 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
|