ytools 0.1.0
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/Rakefile +52 -0
- data/bin/ypath +5 -0
- data/bin/ytemplates +5 -0
- data/lib/VERSION +1 -0
- data/lib/ytools/basecli.rb +67 -0
- data/lib/ytools/errors.rb +7 -0
- data/lib/ytools/path/cli.rb +103 -0
- data/lib/ytools/path/examples.txt +64 -0
- data/lib/ytools/path/executor.rb +58 -0
- data/lib/ytools/path/lexer.rb +166 -0
- data/lib/ytools/path/parser.rb +135 -0
- data/lib/ytools/path/selectors.rb +132 -0
- data/lib/ytools/templates/cli.rb +108 -0
- data/lib/ytools/templates/examples.txt +52 -0
- data/lib/ytools/templates/executor.rb +28 -0
- data/lib/ytools/templates/yaml_object.rb +11 -0
- data/lib/ytools/version.rb +12 -0
- data/lib/ytools/yaml_object.rb +112 -0
- data/lib/ytools/yreader.rb +21 -0
- data/spec/helpers.rb +15 -0
- data/spec/path/executor_spec.rb +45 -0
- data/spec/path/lexer_spec.rb +127 -0
- data/spec/path/parser_spec.rb +104 -0
- data/spec/path/selectors_spec.rb +123 -0
- data/spec/path/yamls/1.yml +2 -0
- data/spec/path/yamls/2.yml +2 -0
- data/spec/path/yamls/3.yml +6 -0
- data/spec/path/yamls/4.yml +12 -0
- data/spec/path/yamls/5.yml +4 -0
- data/spec/yaml_object_spec.rb +61 -0
- metadata +118 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'ytools/errors'
|
2
|
+
require 'ytools/path/lexer'
|
3
|
+
require 'ytools/path/selectors'
|
4
|
+
|
5
|
+
module YTools::Path
|
6
|
+
|
7
|
+
=begin
|
8
|
+
Yaml Path Spec:
|
9
|
+
---------------
|
10
|
+
|
11
|
+
yaml_path:
|
12
|
+
child_selector (child_selector)* <-- '/' path_node is a root
|
13
|
+
| path_node (child_selector)*
|
14
|
+
;
|
15
|
+
|
16
|
+
child_selector:
|
17
|
+
'/' path_node
|
18
|
+
| '//' path_node
|
19
|
+
;
|
20
|
+
|
21
|
+
path_node:
|
22
|
+
PATH_PART index?
|
23
|
+
| NUMBER index?
|
24
|
+
;
|
25
|
+
|
26
|
+
index:
|
27
|
+
LBRACE NUMBER RBRACE
|
28
|
+
;
|
29
|
+
|
30
|
+
=end
|
31
|
+
|
32
|
+
class ParseError < Exception
|
33
|
+
attr_reader :token
|
34
|
+
|
35
|
+
def initialize(message, token)
|
36
|
+
super(message)
|
37
|
+
@token = token
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Parser
|
42
|
+
|
43
|
+
def initialize(path)
|
44
|
+
@lexer = Lexer.new(path)
|
45
|
+
@path = path
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse!
|
49
|
+
selector = nil
|
50
|
+
|
51
|
+
if @lexer.has_next?
|
52
|
+
selector = root_selector
|
53
|
+
end
|
54
|
+
|
55
|
+
while @lexer.has_next?
|
56
|
+
selector.chain(child_selector)
|
57
|
+
end
|
58
|
+
|
59
|
+
selector
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def root_selector
|
64
|
+
if @lexer.peek.type == :path_separator
|
65
|
+
subselector = child_selector
|
66
|
+
if subselector.is_a?(ChildSelector)
|
67
|
+
RootSelector.new.chain(subselector)
|
68
|
+
else
|
69
|
+
subselector
|
70
|
+
end
|
71
|
+
else
|
72
|
+
path_node(false)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def child_selector
|
77
|
+
slash = @lexer.next
|
78
|
+
|
79
|
+
if slash.nil? || slash.type != :path_separator
|
80
|
+
raise ParseError.new("Missing a path separator '/'", slash)
|
81
|
+
end
|
82
|
+
|
83
|
+
if @lexer.peek.nil?
|
84
|
+
raise ParseError.new("Unfinished child path expression", slash)
|
85
|
+
end
|
86
|
+
|
87
|
+
if @lexer.peek.type == :path_separator
|
88
|
+
@lexer.next # Pop off the separator
|
89
|
+
path_node(true)
|
90
|
+
else
|
91
|
+
path_node(false)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def path_node(descendant=nil)
|
96
|
+
node = @lexer.next
|
97
|
+
|
98
|
+
if node.type != :number && node.type != :path_part
|
99
|
+
raise ParseError.new("Unexpected path node type, number or string only", node)
|
100
|
+
end
|
101
|
+
|
102
|
+
sel = if descendant
|
103
|
+
DescendantSelector.new(node.value)
|
104
|
+
else
|
105
|
+
ChildSelector.new(node.value)
|
106
|
+
end
|
107
|
+
|
108
|
+
if !@lexer.peek.nil? && @lexer.peek.type == :lbrace
|
109
|
+
sel.chain(index_selector)
|
110
|
+
end
|
111
|
+
|
112
|
+
sel
|
113
|
+
end
|
114
|
+
|
115
|
+
def index_selector
|
116
|
+
lbrace = @lexer.next
|
117
|
+
|
118
|
+
number = @lexer.next
|
119
|
+
if number.nil?
|
120
|
+
raise ParseError.new("Missing number in unclosed index selector", lbrace)
|
121
|
+
elsif number.type != :number
|
122
|
+
raise ParseError.new("Only numbers are allowed in indices", lbrace)
|
123
|
+
end
|
124
|
+
|
125
|
+
rbrace = @lexer.next
|
126
|
+
if rbrace.nil?
|
127
|
+
raise ParseError.new("Unclosed ']' for index selector", lbrace)
|
128
|
+
elsif rbrace.type != :rbrace
|
129
|
+
raise ParseError.new("Missing ']' character for index", rbrace)
|
130
|
+
end
|
131
|
+
|
132
|
+
IndexSelector.new(number.value)
|
133
|
+
end
|
134
|
+
end # Parser
|
135
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'ytools/errors'
|
2
|
+
require 'ytools/yaml_object'
|
3
|
+
|
4
|
+
module YTools::Path
|
5
|
+
|
6
|
+
class Selector
|
7
|
+
attr_reader :subselector
|
8
|
+
#alias :chain :<<
|
9
|
+
|
10
|
+
def chain(selector)
|
11
|
+
if !chained?
|
12
|
+
@subselector = selector
|
13
|
+
else
|
14
|
+
@subselector.chain(selector)
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def chained?
|
20
|
+
!@subselector.nil?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ChildSelector < Selector
|
25
|
+
attr_reader :child
|
26
|
+
|
27
|
+
def initialize(child)
|
28
|
+
@child = child
|
29
|
+
end
|
30
|
+
|
31
|
+
def select(yaml)
|
32
|
+
if yaml.yhash.has_key?(child)
|
33
|
+
value = yaml[child]
|
34
|
+
if chained?
|
35
|
+
subselector.select(value)
|
36
|
+
else
|
37
|
+
value
|
38
|
+
end
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class IndexSelector < Selector
|
46
|
+
attr_reader :index
|
47
|
+
|
48
|
+
def initialize(index)
|
49
|
+
@index = index
|
50
|
+
end
|
51
|
+
|
52
|
+
def select(array)
|
53
|
+
if array.respond_to?(:[])
|
54
|
+
value = array[index]
|
55
|
+
if chained?
|
56
|
+
subselector.select(value)
|
57
|
+
else
|
58
|
+
value
|
59
|
+
end
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class RootSelector < Selector
|
67
|
+
def select(yaml)
|
68
|
+
if yaml.is_a?(YTools::YamlObject)
|
69
|
+
value = yaml.yroot
|
70
|
+
if chained?
|
71
|
+
subselector.select(value)
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
else
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class DescendantSelector < Selector
|
82
|
+
attr_reader :match
|
83
|
+
|
84
|
+
def initialize(match)
|
85
|
+
@match = match
|
86
|
+
end
|
87
|
+
|
88
|
+
def select(yaml)
|
89
|
+
inner_select(yaml, [])
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
def inner_select(yaml, results)
|
94
|
+
yaml.yhash.each do |key, value|
|
95
|
+
if key == match
|
96
|
+
key_select(value, results)
|
97
|
+
end
|
98
|
+
value_select(value, results)
|
99
|
+
end
|
100
|
+
|
101
|
+
if results.length == 1
|
102
|
+
results[0]
|
103
|
+
else
|
104
|
+
results
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def key_select(value, results)
|
109
|
+
# Filter through the chain, if present
|
110
|
+
if chained?
|
111
|
+
nvalue = subselector.select(value)
|
112
|
+
if !nvalue.nil?
|
113
|
+
results << nvalue
|
114
|
+
end
|
115
|
+
else
|
116
|
+
results << value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def value_select(value, results)
|
121
|
+
if value.is_a?(YTools::YamlObject)
|
122
|
+
inner_select(value, results)
|
123
|
+
elsif value.is_a?(Array)
|
124
|
+
value.each do |v|
|
125
|
+
if v.is_a?(YTools::YamlObject)
|
126
|
+
inner_select(v, results)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ytools/basecli'
|
3
|
+
require 'ytools/errors'
|
4
|
+
require 'ytools/templates/executor'
|
5
|
+
|
6
|
+
module YTools::Templates
|
7
|
+
class CLI < YTools::BaseCLI
|
8
|
+
protected
|
9
|
+
def parse(args)
|
10
|
+
OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{File.basename($0)} [OPTIONS] YAML_FILES"
|
12
|
+
opts.separator <<EOF
|
13
|
+
Description:
|
14
|
+
This tool uses an ERB template file and a set of YAML files to
|
15
|
+
generate a merged file. For convenience, all of the keys in
|
16
|
+
hashes in regular YAML can work like methods in the ERB templates.
|
17
|
+
Thus, the YAML "{ 'a' : {'b' : 3 } }" could be used in an
|
18
|
+
ERB template with "<%= a.b %>" instead of the more verbose hash
|
19
|
+
syntax. Indeed, the root hash values can only be accessed by
|
20
|
+
those method attributes, because the root YAML context object
|
21
|
+
is simply assumed.
|
22
|
+
|
23
|
+
It accepts multiple yaml files, and will merge their contents in the
|
24
|
+
order in which they are given. Thus, files listed later, if their
|
25
|
+
keys conflict with ones listed earlier, override the earlier listed
|
26
|
+
values. If you pass in files that don't exist, no error will be
|
27
|
+
raised unless the '--strict' flag is passed.
|
28
|
+
|
29
|
+
Check out the '--examples' flag for more details.
|
30
|
+
|
31
|
+
Options:
|
32
|
+
EOF
|
33
|
+
opts.on('-t', '--template ERB',
|
34
|
+
"The ERB template file to use for generation") do |t|
|
35
|
+
options[:template] = t
|
36
|
+
end
|
37
|
+
opts.on('-o', '--output FILE',
|
38
|
+
"Write the generated output to a file instead",
|
39
|
+
"of STDOUT") do |o|
|
40
|
+
options[:output] = o
|
41
|
+
end
|
42
|
+
opts.on('-s', '--strict',
|
43
|
+
"Checks to make sure all of the YAML files",
|
44
|
+
"exist before proceeding.") do |s|
|
45
|
+
options[:strict] = true
|
46
|
+
end
|
47
|
+
opts.separator ""
|
48
|
+
|
49
|
+
opts.on('-e', '--examples',
|
50
|
+
"Show some examples on how to use the",
|
51
|
+
"path syntax.") do
|
52
|
+
print_examples(File.dirname(__FILE__))
|
53
|
+
end
|
54
|
+
opts.on('-v', '--version',
|
55
|
+
"Show the version information") do |v|
|
56
|
+
print_version
|
57
|
+
end
|
58
|
+
opts.on('-d', '--debug',
|
59
|
+
"Prints out the merged yaml as a",
|
60
|
+
"ruby object to STDERR.") do |d|
|
61
|
+
options[:debug] = true
|
62
|
+
end
|
63
|
+
opts.on('-h', '--help',
|
64
|
+
"Show this help message.") do
|
65
|
+
puts opts
|
66
|
+
exit 0
|
67
|
+
end
|
68
|
+
end.parse!(args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate(args)
|
72
|
+
if options[:template].nil?
|
73
|
+
raise YTools::ConfigurationError.new("No template file was indicated")
|
74
|
+
end
|
75
|
+
|
76
|
+
if !File.exists?(options[:template])
|
77
|
+
raise YTools::ConfigurationError.new("Unable to locate the template file: #{options[:template]}")
|
78
|
+
end
|
79
|
+
|
80
|
+
if options[:output] &&
|
81
|
+
!File.exists?(File.dirname(options[:output]))
|
82
|
+
raise YTools::ConfigurationError.new("The output directory doesn't exist: #{option[:output]}")
|
83
|
+
end
|
84
|
+
|
85
|
+
if args.length == 0
|
86
|
+
raise YTools::ConfigurationError.new("No YAML files were given")
|
87
|
+
end
|
88
|
+
|
89
|
+
if options[:strict]
|
90
|
+
args.each do |file|
|
91
|
+
if !File.exists?(file)
|
92
|
+
raise YTools::ConfigurationError.new("Unable to locate YAML file: #{file}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def execute(args)
|
99
|
+
executor = Executor.new(options[:template], args)
|
100
|
+
|
101
|
+
if options[:debug]
|
102
|
+
STDERR.puts executor.yaml_object.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
executor.write!(options[:output])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
Assuming a YAML file like the following:
|
2
|
+
|
3
|
+
---
|
4
|
+
this:
|
5
|
+
is:
|
6
|
+
- a
|
7
|
+
- very
|
8
|
+
- simple
|
9
|
+
yaml: file
|
10
|
+
but: It's
|
11
|
+
pretty:
|
12
|
+
easy: 42
|
13
|
+
to-read: 'right?'
|
14
|
+
---
|
15
|
+
|
16
|
+
You can then create a template to access these values:
|
17
|
+
|
18
|
+
---
|
19
|
+
You can write out <%= this.is[0] %> simple template
|
20
|
+
<%= this.yaml %> that uses the YAML as its bound object.
|
21
|
+
It's fairly straight forward, <%= this.pretty['to-read']%>
|
22
|
+
---
|
23
|
+
|
24
|
+
It would output:
|
25
|
+
|
26
|
+
---
|
27
|
+
You can write out a simple template
|
28
|
+
file that uses the YAML as its bound object.
|
29
|
+
It's fairly straight forward, right?
|
30
|
+
---
|
31
|
+
|
32
|
+
You can, of course use all of the syntax available
|
33
|
+
in a regular ERB template so you could do something
|
34
|
+
like:
|
35
|
+
|
36
|
+
---
|
37
|
+
<% 0.upto(this.pretty.easy) do |i| %>
|
38
|
+
<% if i == 42 %>
|
39
|
+
The secret of the universe is <%= i %>!
|
40
|
+
<%= this.but %> <%= this.is[2] %> <%= this.is[3] %>
|
41
|
+
<%= this.pretty['to-read'] %>
|
42
|
+
<% end %>
|
43
|
+
<% end %>
|
44
|
+
---
|
45
|
+
|
46
|
+
That would output:
|
47
|
+
|
48
|
+
---
|
49
|
+
The secret of the universe is 42!
|
50
|
+
It's very simple
|
51
|
+
right?
|
52
|
+
---
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'ytools/yaml_object'
|
3
|
+
require 'ytools/templates/yaml_object'
|
4
|
+
|
5
|
+
module YTools::Templates
|
6
|
+
class Executor
|
7
|
+
attr_reader :template, :yaml_object
|
8
|
+
|
9
|
+
def initialize(template, files)
|
10
|
+
@template = template
|
11
|
+
@yaml_object = YTools::YamlObject.from_files(files)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write!(outfile)
|
15
|
+
tcontents = nil
|
16
|
+
File.open(template, 'r') { |f| tcontents = f.read}
|
17
|
+
|
18
|
+
generator = ERB.new(tcontents)
|
19
|
+
output = generator.result(yaml_object.erb_binding)
|
20
|
+
|
21
|
+
if outfile
|
22
|
+
File.open(outfile, 'w') {|f| f.write(output)}
|
23
|
+
else
|
24
|
+
puts output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|