sapluuna 0.1.4
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 +2 -0
- data/Gemfile +3 -0
- data/README.md +11 -0
- data/Rakefile +47 -0
- data/bin/sapluuna +11 -0
- data/lib/sapluuna/class_helpers.rb +44 -0
- data/lib/sapluuna/cli.rb +61 -0
- data/lib/sapluuna/context.rb +94 -0
- data/lib/sapluuna/ip.rb +51 -0
- data/lib/sapluuna/parser.rb +74 -0
- data/lib/sapluuna/sapluuna.rb +91 -0
- data/sapluuna.gemspec +18 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f176e218e8e443cec9eaa8e94f6ca233ae9f8014
|
4
|
+
data.tar.gz: 3f783c2bc0f48f9297a052bd0d8b55352b4a2833
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f174827d07bc20c043cb03f6c792518dc2b7ef42d2bb86aa6b53b521792aa57a892f3e7d38263b8a5ba2756c006eea10e24f0bbf518e90a12eaff416136f1b78
|
7
|
+
data.tar.gz: 364fc6803738df805c191364e8c8a9eecb1693e8b3159b003fda9aba93d8a908006247b3289d9bd746eff16d5883dd588bb0203a56c4876fa6aa50ffadd93415
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Sapluuna
|
2
|
+
|
3
|
+
Silly little template based configuration maker
|
4
|
+
|
5
|
+
* Anything outside {{{ ... }}} is comment and won't be in generated file
|
6
|
+
* Inside {{{ }}} you can have <% foo %> which is just ruby
|
7
|
+
* For method_missing in <% foo %> we try variable[name] hash, given to constrcutor, i.e. Sapluuna.new variables: {replace_this: 'with_this'} .... <% replace_this %> works
|
8
|
+
* {{{ can be followed by negative or positive labels, if labels match to those given to constructor {{{ }}} is evaluated, otherwise ignored
|
9
|
+
* rationale for labels is {{{ PE ..... }}} or {{{ Finland Sweden ..... }}} to conditionally evaluate blocks
|
10
|
+
* You can query the instance on what variables are needed when labels X are set, use-case is in say in webUI to automatically generate form with all variables template needs
|
11
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
begin
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'bundler'
|
4
|
+
# Bundler.setup
|
5
|
+
rescue LoadError
|
6
|
+
warn 'bundler missing'
|
7
|
+
end
|
8
|
+
|
9
|
+
gemspec = eval(File.read(Dir['*.gemspec'].first))
|
10
|
+
file = [gemspec.name, gemspec.version].join('-') + '.gem'
|
11
|
+
|
12
|
+
desc 'Validate gemspec'
|
13
|
+
task :gemspec do
|
14
|
+
gemspec.validate
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Run minitest'
|
18
|
+
task :test do
|
19
|
+
Rake::TestTask.new do |t|
|
20
|
+
t.libs.push "lib"
|
21
|
+
t.test_files = FileList['spec/*_spec.rb']
|
22
|
+
t.verbose = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Build gem'
|
27
|
+
task :build do
|
28
|
+
system "gem build #{gemspec.name}.gemspec"
|
29
|
+
FileUtils.mkdir_p 'gems'
|
30
|
+
FileUtils.mv file, 'gems'
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Install gem'
|
34
|
+
task :install => :build do
|
35
|
+
system "sudo -Es sh -c \'umask 022; gem install gems/#{file}\'"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Remove gems'
|
39
|
+
task :clean do
|
40
|
+
FileUtils.rm_rf 'gems'
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Push to rubygems'
|
44
|
+
task :push do
|
45
|
+
system "gem push gems/#{file}"
|
46
|
+
system "git tag #{gemspec.version}"
|
47
|
+
end
|
data/bin/sapluuna
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'ip'
|
2
|
+
|
3
|
+
class Sapluuna
|
4
|
+
module ClassHelpers
|
5
|
+
|
6
|
+
class ::String
|
7
|
+
def ip
|
8
|
+
IP.new(self).ip
|
9
|
+
end
|
10
|
+
def acl
|
11
|
+
IP.new(self).acl
|
12
|
+
end
|
13
|
+
def net
|
14
|
+
IP.new(self).net
|
15
|
+
end
|
16
|
+
def cidr
|
17
|
+
IP.new(self).cidr
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ::Array
|
22
|
+
def as_ip format_string
|
23
|
+
as_method 'ip', format_string
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_acl format_string
|
27
|
+
as_method 'acl', format_string
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_cidr format_string
|
31
|
+
as_method 'cidr', format_string
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def as_method method, format_string
|
37
|
+
map do |str|
|
38
|
+
format_string % str.send(method)
|
39
|
+
end.join("\n")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/sapluuna/cli.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative 'sapluuna'
|
2
|
+
begin
|
3
|
+
require 'slop'
|
4
|
+
rescue LoadError
|
5
|
+
warn "sudo gem install slop ## required by sapluuna CLI"
|
6
|
+
exit 42
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
class Sapluuna
|
12
|
+
class CLI
|
13
|
+
ROOT = '.'
|
14
|
+
attr_reader :debug
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@opts = opts_parse
|
18
|
+
args = @opts.arguments
|
19
|
+
@file = args.shift
|
20
|
+
@labels = @opts[:label].split(/[,\s]+/) if @opts[:label]
|
21
|
+
@vars = {}
|
22
|
+
@disco = @opts[:variables]
|
23
|
+
args.each do |var|
|
24
|
+
name, value = var.split '='
|
25
|
+
@vars[name.to_sym] = value
|
26
|
+
end
|
27
|
+
if @opts.debug?
|
28
|
+
@debug = true
|
29
|
+
Log.level = Logger::DEBUG
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
raise MissingOption, 'File is mandatory argument' unless @file
|
35
|
+
sap = Sapluuna.new labels: @labels, variables: @vars,
|
36
|
+
discover_variables: @disco,
|
37
|
+
root_directory: (@opts[:root] or ROOT)
|
38
|
+
cfg = sap.parse File.read(@file)
|
39
|
+
puts @disco ? sap.discovered_variables.keys : cfg
|
40
|
+
rescue => error
|
41
|
+
crash error
|
42
|
+
raise
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def opts_parse
|
48
|
+
Slop.parse do |o|
|
49
|
+
o.banner = 'Usage: sapluuna [OPTIONS] FILE [variables]'
|
50
|
+
o.bool '-d', '--debug', 'turn on debugging'
|
51
|
+
o.string '-l', '--label', 'commma separated list of labels'
|
52
|
+
o.bool '-v', '--variables', 'displays required variables'
|
53
|
+
o.string '-r', '--root', 'root directory for template import'
|
54
|
+
o.on '-h', '--help' do puts o; exit; end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def crash error
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class Sapluuna
|
2
|
+
class Context
|
3
|
+
attr_reader :discovered_variables, :variables
|
4
|
+
|
5
|
+
RootDirectory = '.'
|
6
|
+
|
7
|
+
class VariableMissing < Error; end
|
8
|
+
|
9
|
+
def initialize opts
|
10
|
+
@opts = opts.dup
|
11
|
+
@discover_variables = opts.delete :discover_variables
|
12
|
+
@variables = (opts.delete(:variables) or {})
|
13
|
+
@root_directory = (opts.delete(:root_directory) or RootDirectory)
|
14
|
+
@output = ''
|
15
|
+
@discovered_variables = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def cfg value
|
19
|
+
@output << value
|
20
|
+
end
|
21
|
+
|
22
|
+
def code value
|
23
|
+
@output << eval(value).to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def str
|
27
|
+
@output
|
28
|
+
end
|
29
|
+
|
30
|
+
def are value
|
31
|
+
[:are, value]
|
32
|
+
end
|
33
|
+
|
34
|
+
def is value
|
35
|
+
[:is, value]
|
36
|
+
end
|
37
|
+
|
38
|
+
def silent *args
|
39
|
+
""
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def import file
|
45
|
+
template = File.read resolve_file(file)
|
46
|
+
@opts[:variables] = @variables
|
47
|
+
@opts[:root_directory] = @root_directory.dup
|
48
|
+
Log.debug "importing #{file}"
|
49
|
+
sapl = Sapluuna.new @opts
|
50
|
+
output = sapl.parse template
|
51
|
+
@discovered_variables.merge! sapl.discovered_variables
|
52
|
+
add_indent output.lines, @output.lines.last
|
53
|
+
rescue => error
|
54
|
+
raise error, "#{error.message} (while reading #{file})"
|
55
|
+
end
|
56
|
+
|
57
|
+
def resolve_file file
|
58
|
+
# should we avoid ../../../etc/passwd style input here?
|
59
|
+
# why? we control templates?
|
60
|
+
File.join @root_directory, file
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_indent output, indent_hint
|
64
|
+
return output.join.chomp if output.size < 2 or not indent_hint
|
65
|
+
indent_size = indent_hint.match(/\A\s*/)[0].size
|
66
|
+
first_line = output[0]
|
67
|
+
output = output[1..-1].map { |line| ' ' * indent_size + line }
|
68
|
+
output.unshift(first_line).join.chomp
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing method, *args
|
72
|
+
if Array === args.first
|
73
|
+
value = args.first.last
|
74
|
+
case args.first.first
|
75
|
+
when :is
|
76
|
+
@variables[method] = value
|
77
|
+
when :are
|
78
|
+
@variables[method] = value.to_s.strip.split(/\s+/)
|
79
|
+
end
|
80
|
+
""
|
81
|
+
elsif @variables[method]
|
82
|
+
@variables[method]
|
83
|
+
else
|
84
|
+
if @discover_variables
|
85
|
+
args ||= ['']
|
86
|
+
@discovered_variables[method] = args[0] unless @discovered_variables.has_key? method
|
87
|
+
""
|
88
|
+
else
|
89
|
+
raise VariableMissing, "variable '#{method}' required, but not given"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/sapluuna/ip.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
class Sapluuna
|
4
|
+
class IP < IPAddr
|
5
|
+
|
6
|
+
def initialize(addr = '::', family = Socket::AF_UNSPEC)
|
7
|
+
addr_org, _family_org = addr, family
|
8
|
+
prefix, _prefixlen = addr_org.split('/')
|
9
|
+
@addr_org = prefix
|
10
|
+
super
|
11
|
+
if @family == Socket::AF_UNSPEC or @family == Socket::AF_INET
|
12
|
+
@addr_org = in_addr(@addr_org)
|
13
|
+
else
|
14
|
+
@addr_org = in6_addr(@addr_org)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def ip
|
19
|
+
addr_tmp = @addr
|
20
|
+
@addr = @addr_org
|
21
|
+
ip = self.to_s
|
22
|
+
@addr = addr_tmp
|
23
|
+
ip
|
24
|
+
end
|
25
|
+
|
26
|
+
def mask_cidr
|
27
|
+
@mask_addr.to_s(2).delete('0').size
|
28
|
+
end
|
29
|
+
|
30
|
+
def mask_wild
|
31
|
+
_to_string ~@mask_addr
|
32
|
+
end
|
33
|
+
|
34
|
+
def mask_net
|
35
|
+
_to_string @mask_addr
|
36
|
+
end
|
37
|
+
|
38
|
+
def cidr
|
39
|
+
ip + '/' + mask_cidr.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def acl
|
43
|
+
ip + ' '+ mask_wild
|
44
|
+
end
|
45
|
+
|
46
|
+
def net
|
47
|
+
ip + ' ' + mask_net
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class Sapluuna
|
4
|
+
class Parser
|
5
|
+
CODE_OPEN = '<%\s*'
|
6
|
+
CODE_CLOSE = '\s*%>'
|
7
|
+
TEMPLATE_OPEN = '^\s*{{{[\t ]*'
|
8
|
+
TEMPLATE_CLOSE = '\s*}}}\s*$'
|
9
|
+
NEW_LINE = "\n"
|
10
|
+
class ParserError < Error; end
|
11
|
+
class UnterminatedBlock < ParserError; end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@sc = StringScanner.new ''
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse input
|
18
|
+
level = 0
|
19
|
+
cfg = []
|
20
|
+
@sc.string = input
|
21
|
+
loop do
|
22
|
+
if @sc.scan_until Regexp.new(TEMPLATE_OPEN)
|
23
|
+
cfg << [:template, get_labels, get_template(level)]
|
24
|
+
else
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@sc.string = '' # no need to keep it in memory
|
29
|
+
cfg
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def get_labels
|
35
|
+
labels = @sc.scan_until Regexp.new(NEW_LINE)
|
36
|
+
labels.strip.split(/\s+/)
|
37
|
+
end
|
38
|
+
|
39
|
+
def re_combine *args
|
40
|
+
re = args.map do |arg|
|
41
|
+
'(?:%s)' % arg
|
42
|
+
end.join('|')
|
43
|
+
Regexp.new re
|
44
|
+
end
|
45
|
+
|
46
|
+
def clean_scan scan
|
47
|
+
endstr = -(@sc.matched.size+1)
|
48
|
+
scan[0..endstr]
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_template level
|
52
|
+
cfg = []
|
53
|
+
loop do
|
54
|
+
scan = @sc.scan_until re_combine(CODE_OPEN, TEMPLATE_OPEN, TEMPLATE_CLOSE)
|
55
|
+
raise UnterminatedBlock, "template at #{level}, #{@sc.pos} runs forever" unless scan
|
56
|
+
cfg << [:cfg, clean_scan(scan)]
|
57
|
+
case @sc.matched
|
58
|
+
|
59
|
+
when Regexp.new(CODE_OPEN)
|
60
|
+
scan = @sc.scan_until Regexp.new(CODE_CLOSE)
|
61
|
+
raise UnterminatedBlock, "code at #{level}, #{@sc.pos} runs forever" unless scan
|
62
|
+
cfg << [:code, clean_scan(scan)]
|
63
|
+
|
64
|
+
when Regexp.new(TEMPLATE_OPEN)
|
65
|
+
cfg << [:template, get_labels, get_template(level+1)]
|
66
|
+
|
67
|
+
when Regexp.new(TEMPLATE_CLOSE)
|
68
|
+
return cfg
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative 'class_helpers'
|
3
|
+
|
4
|
+
class Sapluuna
|
5
|
+
|
6
|
+
include ClassHelpers
|
7
|
+
class Error < StandardError; end
|
8
|
+
class InvalidOption < StandardError; end
|
9
|
+
class MissingOption < StandardError; end
|
10
|
+
class InvalidType < Error; end
|
11
|
+
Log = Logger.new STDERR
|
12
|
+
Log.level = Logger::WARN
|
13
|
+
|
14
|
+
def initialize opts
|
15
|
+
@context = (opts[:context] or Context)
|
16
|
+
@want_labels = read_labels (opts[:labels] or [])
|
17
|
+
@context = @context.new(opts) if @context.class == Class
|
18
|
+
@parser = Parser.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def discovered_variables
|
22
|
+
@context.discovered_variables
|
23
|
+
end
|
24
|
+
|
25
|
+
def variables
|
26
|
+
@context.variables
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse input
|
30
|
+
@parser.parse(input).each do |cfg|
|
31
|
+
type = cfg.shift
|
32
|
+
case type
|
33
|
+
when :template
|
34
|
+
template cfg
|
35
|
+
else
|
36
|
+
raise InvalidType, "#{type} was not recognized by parser"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@context.str
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def read_labels labels
|
45
|
+
pos, neg = [], []
|
46
|
+
labels.each do |label|
|
47
|
+
label[0] == '!' ? neg.push(label[1..-1]) : pos.push(label)
|
48
|
+
end
|
49
|
+
[pos, neg]
|
50
|
+
end
|
51
|
+
|
52
|
+
def template templ
|
53
|
+
return unless valid_labels? read_labels(templ.shift)
|
54
|
+
templ.shift.each do |t|
|
55
|
+
type = t.shift
|
56
|
+
case type
|
57
|
+
when :code
|
58
|
+
@context.code t.last
|
59
|
+
when :cfg
|
60
|
+
@context.cfg t.last
|
61
|
+
when :template
|
62
|
+
template t
|
63
|
+
else
|
64
|
+
raise InvalidType, "#{type} was not recognized by parser"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def valid_labels? got_labels
|
70
|
+
# first/[0] is our positive labels (label)
|
71
|
+
# last/[1] is our negative labels (!label)
|
72
|
+
|
73
|
+
# template has label which we explcitly don't want
|
74
|
+
return false if (got_labels[0] & @want_labels[1]).size > 0
|
75
|
+
|
76
|
+
# template forbids label which we explicitly want
|
77
|
+
return false if (got_labels[1] & @want_labels[0]).size > 0
|
78
|
+
|
79
|
+
# template requires no labels
|
80
|
+
return true if got_labels[0].empty?
|
81
|
+
|
82
|
+
# template requires labels, do we want at least one of them?
|
83
|
+
return false unless (got_labels[0] & @want_labels[0]).size > 0
|
84
|
+
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
require_relative 'parser'
|
91
|
+
require_relative 'context'
|
data/sapluuna.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'sapluuna'
|
3
|
+
s.version = '0.1.4'
|
4
|
+
s.licenses = %w( Apache-2.0 )
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
s.authors = [ 'Saku Ytti' ]
|
7
|
+
s.email = %w( saku@ytti.fi )
|
8
|
+
s.homepage = 'http://github.com/ytti/sapluuna'
|
9
|
+
s.summary = 'Template parser'
|
10
|
+
s.description = 'Template based network configuration generator'
|
11
|
+
s.rubyforge_project = s.name
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.executables = %w( sapluuna )
|
14
|
+
s.require_path = 'lib/sapluuna'
|
15
|
+
|
16
|
+
s.required_ruby_version = '>= 2.0.0'
|
17
|
+
s.add_runtime_dependency 'slop', '~> 4.0'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sapluuna
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Saku Ytti
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: slop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
description: Template based network configuration generator
|
28
|
+
email:
|
29
|
+
- saku@ytti.fi
|
30
|
+
executables:
|
31
|
+
- sapluuna
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".gitignore"
|
36
|
+
- Gemfile
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- bin/sapluuna
|
40
|
+
- lib/sapluuna/class_helpers.rb
|
41
|
+
- lib/sapluuna/cli.rb
|
42
|
+
- lib/sapluuna/context.rb
|
43
|
+
- lib/sapluuna/ip.rb
|
44
|
+
- lib/sapluuna/parser.rb
|
45
|
+
- lib/sapluuna/sapluuna.rb
|
46
|
+
- sapluuna.gemspec
|
47
|
+
homepage: http://github.com/ytti/sapluuna
|
48
|
+
licenses:
|
49
|
+
- Apache-2.0
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib/sapluuna
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 2.0.0
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project: sapluuna
|
67
|
+
rubygems_version: 2.2.2
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Template parser
|
71
|
+
test_files: []
|