yes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yes/cli'
3
+ YES.cli(*ARGV)
4
+
@@ -0,0 +1,21 @@
1
+ ---
2
+ /*:
3
+ type: map
4
+
5
+ /*/type:
6
+ type: str
7
+
8
+ /*/required:
9
+ type: bool
10
+ required: false
11
+
12
+ /*/range:
13
+ type: str
14
+ pattern: '^\d+\.\.(\d+|n)$'
15
+
16
+ /*/pattern:
17
+ type: str
18
+
19
+ /*/fnmatch:
20
+ type: str
21
+
@@ -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
+
@@ -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