cfoo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.simplecov +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +676 -0
- data/README.md +166 -0
- data/Rakefile +10 -0
- data/TODO +29 -0
- data/bin/cfoo +26 -0
- data/cfoo.gemspec +35 -0
- data/features/attribute_expansion.feature +43 -0
- data/features/convert_yaml_to_json.feature +69 -0
- data/features/el_escaping.feature +21 -0
- data/features/map_reference_expansion.feature +68 -0
- data/features/modules.feature +101 -0
- data/features/parse_files.feature +46 -0
- data/features/reference_expansion.feature +49 -0
- data/features/step_definitions/cfoo_steps.rb +107 -0
- data/features/support/env.rb +5 -0
- data/features/yamly_shortcuts.feature +132 -0
- data/lib/cfoo.rb +11 -0
- data/lib/cfoo/cfoo.rb +15 -0
- data/lib/cfoo/el_parser.rb +105 -0
- data/lib/cfoo/file_system.rb +28 -0
- data/lib/cfoo/module.rb +25 -0
- data/lib/cfoo/parser.rb +82 -0
- data/lib/cfoo/processor.rb +38 -0
- data/lib/cfoo/project.rb +16 -0
- data/lib/cfoo/renderer.rb +9 -0
- data/lib/cfoo/version.rb +3 -0
- data/lib/cfoo/yaml.rb +34 -0
- data/lib/cfoo/yaml_parser.rb +16 -0
- data/spec/cfoo/cfoo_spec.rb +31 -0
- data/spec/cfoo/el_parser_spec.rb +47 -0
- data/spec/cfoo/file_system_spec.rb +61 -0
- data/spec/cfoo/module_spec.rb +16 -0
- data/spec/cfoo/parser_spec.rb +148 -0
- data/spec/cfoo/processor_spec.rb +52 -0
- data/spec/cfoo/project_spec.rb +16 -0
- data/spec/cfoo/renderer_spec.rb +19 -0
- data/spec/cfoo/yaml_parser_spec.rb +79 -0
- data/spec/spec_helper.rb +2 -0
- metadata +235 -0
data/lib/cfoo/cfoo.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Cfoo
|
2
|
+
class Cfoo
|
3
|
+
def initialize(processor, renderer, stdout)
|
4
|
+
@processor, @renderer, @stdout = processor, renderer, stdout
|
5
|
+
end
|
6
|
+
|
7
|
+
def process(*filenames)
|
8
|
+
@stdout.puts(@renderer.render @processor.process(*filenames))
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_project
|
12
|
+
@stdout.puts(@renderer.render @processor.process_all)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'parslet'
|
3
|
+
|
4
|
+
module Cfoo
|
5
|
+
class ElParser < Parslet::Parser
|
6
|
+
|
7
|
+
def self.parse(string)
|
8
|
+
return string if string.empty?
|
9
|
+
|
10
|
+
parser = ElParser.new
|
11
|
+
transform = ElTransform.new
|
12
|
+
|
13
|
+
tree = parser.parse(string)
|
14
|
+
transform.apply(tree)
|
15
|
+
#rescue Parslet::ParseFailed => failure
|
16
|
+
# #TODO: handle this properly
|
17
|
+
end
|
18
|
+
|
19
|
+
rule(:space) { match('\s') }
|
20
|
+
rule(:space?) { space.maybe }
|
21
|
+
rule(:escaped_dollar) { str('\$').as(:escaped_dollar) }
|
22
|
+
rule(:lone_backslash) { str('\\').as(:lone_backslash) }
|
23
|
+
rule(:lparen) { str('(') }
|
24
|
+
rule(:rparen) { str(')') }
|
25
|
+
rule(:lbracket) { str('[') }
|
26
|
+
rule(:rbracket) { str(']') }
|
27
|
+
rule(:dot) { str('.') }
|
28
|
+
rule(:identifier) { match['a-zA-Z:'].repeat(1).as(:identifier) }
|
29
|
+
rule(:text) { match['^\\\\$'].repeat(1).as(:text) }
|
30
|
+
rule(:attribute_reference) do
|
31
|
+
(
|
32
|
+
expression.as(:reference) >> (
|
33
|
+
str(".") >> identifier.as(:attribute) |
|
34
|
+
str("[") >> expression.as(:attribute) >> str("]")
|
35
|
+
)
|
36
|
+
).as(:attribute_reference)
|
37
|
+
end
|
38
|
+
rule(:mapping) do
|
39
|
+
(
|
40
|
+
expression.as(:map) >>
|
41
|
+
str("[") >> expression.as(:key) >> str("]") >>
|
42
|
+
str("[") >> expression.as(:value) >> str("]")
|
43
|
+
).as(:mapping)
|
44
|
+
end
|
45
|
+
rule(:reference) do
|
46
|
+
expression.as(:reference)
|
47
|
+
end
|
48
|
+
|
49
|
+
rule(:expression) { el | identifier }
|
50
|
+
rule(:el) do
|
51
|
+
str("$(") >> ( mapping | attribute_reference | reference ) >> str(")")
|
52
|
+
end
|
53
|
+
rule(:string) do
|
54
|
+
( escaped_dollar | lone_backslash | el | text ).repeat.as(:string)
|
55
|
+
end
|
56
|
+
|
57
|
+
root(:string)
|
58
|
+
end
|
59
|
+
|
60
|
+
class ElTransform < Parslet::Transform
|
61
|
+
|
62
|
+
rule(:escaped_dollar => simple(:dollar)) { "$" }
|
63
|
+
rule(:lone_backslash => simple(:backslash)) { "\\" }
|
64
|
+
|
65
|
+
rule(:identifier => simple(:identifier)) do
|
66
|
+
identifier.str
|
67
|
+
end
|
68
|
+
|
69
|
+
rule(:text => simple(:text)) do
|
70
|
+
text.str
|
71
|
+
end
|
72
|
+
|
73
|
+
rule(:reference => subtree(:reference)) do
|
74
|
+
{ "Ref" => reference }
|
75
|
+
end
|
76
|
+
|
77
|
+
rule(:mapping => { :map => subtree(:map), :key => subtree(:key), :value => subtree(:value)}) do
|
78
|
+
{ "Fn::FindInMap" => [map, key, value] }
|
79
|
+
end
|
80
|
+
|
81
|
+
rule(:attribute_reference => { :reference => subtree(:reference), :attribute => subtree(:attribute)}) do
|
82
|
+
{ "Fn::GetAtt" => [ reference, attribute ] }
|
83
|
+
end
|
84
|
+
|
85
|
+
rule(:string => subtree(:string)) do
|
86
|
+
# Join escaped EL with adjacent strings
|
87
|
+
parts = string.inject(['']) do |combined_parts, part|
|
88
|
+
previous = combined_parts.pop
|
89
|
+
if previous.class == String && part.class == String
|
90
|
+
combined_parts << previous + part
|
91
|
+
else
|
92
|
+
combined_parts << previous << part
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
parts.reject! {|part| part.empty? }
|
97
|
+
|
98
|
+
if parts.size == 1
|
99
|
+
parts.first
|
100
|
+
else
|
101
|
+
{ "Fn::Join" => [ "", parts ] }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'cfoo/yaml_parser'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
class FileSystem
|
5
|
+
def initialize(project_root, yaml_parser)
|
6
|
+
@project_root, @yaml_parser = project_root, yaml_parser
|
7
|
+
end
|
8
|
+
|
9
|
+
def resolve_file(file_name)
|
10
|
+
"#{@project_root}/#{file_name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_file(file_name)
|
14
|
+
@yaml_parser.load_file(resolve_file file_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def glob_relative(path)
|
18
|
+
absolute_files = Dir.glob(resolve_file path)
|
19
|
+
absolute_files.map do |file|
|
20
|
+
file.gsub(@project_root + '/', '')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def list(path)
|
25
|
+
Dir.glob("#{resolve_file path}/*")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/cfoo/module.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Cfoo
|
2
|
+
class Module
|
3
|
+
attr_reader :dir
|
4
|
+
|
5
|
+
def initialize(dir, file_system)
|
6
|
+
@dir, @file_system = dir, file_system
|
7
|
+
end
|
8
|
+
|
9
|
+
def files
|
10
|
+
@file_system.glob_relative("#{dir}/*.yml")
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
eql? other
|
15
|
+
end
|
16
|
+
|
17
|
+
def eql?(other)
|
18
|
+
dir = other.dir
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"Module[#{dir}]"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/cfoo/parser.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'cfoo/el_parser'
|
2
|
+
require 'cfoo/yaml'
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def expand_el
|
6
|
+
raise Cfoo::Parser::ElParseError, "Couldn't parse object '#{self}'. I don't know how to parse an instance of '#{self.class}'"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Fixnum
|
11
|
+
def expand_el
|
12
|
+
self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class FalseClass
|
17
|
+
def expand_el
|
18
|
+
to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class TrueClass
|
23
|
+
def expand_el
|
24
|
+
to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class String
|
29
|
+
def expand_el
|
30
|
+
Cfoo::ElParser.parse(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Array
|
35
|
+
def expand_el
|
36
|
+
map {|element| element.expand_el }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Hash
|
41
|
+
def expand_el
|
42
|
+
Hash[map do |key, value|
|
43
|
+
[ key, value.expand_el ]
|
44
|
+
end]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module YAML
|
49
|
+
class DomainType
|
50
|
+
def expand_el
|
51
|
+
case type_id
|
52
|
+
when "Ref"
|
53
|
+
{ "Ref" => value.expand_el }
|
54
|
+
when /^(Base64|FindInMap|GetAtt|GetAZs|Join)$/
|
55
|
+
{ "Fn::#{type_id}" => value.expand_el }
|
56
|
+
when "Concat"
|
57
|
+
{ "Fn::Join" => ['', value.expand_el] }
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Cfoo
|
66
|
+
class Parser
|
67
|
+
class ElParseError < RuntimeError
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(file_system)
|
71
|
+
@file_system = file_system
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_file(file_name)
|
75
|
+
@file_system.parse_file(file_name).expand_el
|
76
|
+
rescue Parslet::ParseFailed => failure
|
77
|
+
puts "Failed to parse '#{file_name}':"
|
78
|
+
puts failure.cause.ascii_tree
|
79
|
+
raise failure
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
class Hash
|
3
|
+
def deep_merge(other)
|
4
|
+
merge(other) do |key, our_item, their_item|
|
5
|
+
if our_item.respond_to? :deep_merge
|
6
|
+
our_item.deep_merge(their_item)
|
7
|
+
elsif our_item.respond_to? :concat
|
8
|
+
our_item.concat(their_item).uniq
|
9
|
+
else
|
10
|
+
their_item
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Cfoo
|
17
|
+
class Processor
|
18
|
+
def initialize(parser, project)
|
19
|
+
@parser, @project = parser, project
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(*filenames)
|
23
|
+
project_map = { "AWSTemplateFormatVersion" => "2010-09-09" }
|
24
|
+
filenames.each do |filename|
|
25
|
+
module_map = @parser.parse_file filename
|
26
|
+
project_map = project_map.deep_merge module_map
|
27
|
+
end
|
28
|
+
project_map
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_all
|
32
|
+
project_files = @project.modules.inject([]) do |all_files, mod|
|
33
|
+
all_files += mod.files
|
34
|
+
end
|
35
|
+
process *project_files
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/cfoo/project.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'cfoo/module'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
class Project
|
5
|
+
def initialize(file_system)
|
6
|
+
@file_system = file_system
|
7
|
+
end
|
8
|
+
|
9
|
+
def modules
|
10
|
+
module_dirs = @file_system.glob_relative("modules/*")
|
11
|
+
module_dirs.map do |dir|
|
12
|
+
Module.new(dir, @file_system)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cfoo/version.rb
ADDED
data/lib/cfoo/yaml.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module YAML
|
4
|
+
|
5
|
+
def self.add_domain_type_that_gets_loaded_like_in_ruby_1_8(domain_type)
|
6
|
+
add_domain_type "", domain_type do |tag, value|
|
7
|
+
DomainType.create(domain_type, value)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class DomainType
|
12
|
+
attr_accessor :domain, :type_id, :value
|
13
|
+
|
14
|
+
def self.create(type_id, value)
|
15
|
+
type = self.allocate
|
16
|
+
type.domain = "yaml.org,2002"
|
17
|
+
type.type_id = type_id
|
18
|
+
type.value = value
|
19
|
+
type
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(other)
|
23
|
+
eq? other
|
24
|
+
end
|
25
|
+
|
26
|
+
def eq?(other)
|
27
|
+
if other.respond_to?(:domain) && other.respond_to?(:type_id) && other.respond_to?(:value)
|
28
|
+
domain == other.domain && type_id == other.type_id && value == other.value
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'cfoo/yaml'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
class YamlParser
|
5
|
+
|
6
|
+
CFN_DOMAIN_TYPES = [ "GetAZs", "Ref", "Join", "Concat", "GetAtt", "FindInMap", "Base64" ]
|
7
|
+
|
8
|
+
def load_file(file_name)
|
9
|
+
#TODO: raise errors if "value" isn't the right type
|
10
|
+
CFN_DOMAIN_TYPES.each do |domain_type|
|
11
|
+
YAML.add_domain_type_that_gets_loaded_like_in_ruby_1_8(domain_type)
|
12
|
+
end
|
13
|
+
YAML.load_file(file_name)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
describe Cfoo do
|
5
|
+
let(:processor) { double('processor') }
|
6
|
+
let(:renderer) { double('renderer') }
|
7
|
+
let(:stdout) { double('stdout') }
|
8
|
+
let(:cfoo) { Cfoo.new(processor, renderer, stdout) }
|
9
|
+
|
10
|
+
describe "#process" do
|
11
|
+
it "processes the specified files" do
|
12
|
+
output_map = double("output_map")
|
13
|
+
processor.should_receive(:process).with("1.yml", "2.yml").and_return output_map
|
14
|
+
renderer.should_receive(:render).with(output_map).and_return "cfn_template"
|
15
|
+
stdout.should_receive(:puts).with "cfn_template"
|
16
|
+
cfoo.process("1.yml", "2.yml")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#build_project" do
|
21
|
+
# TODO: but it doesn't know about the project!
|
22
|
+
it "processes all files in the project" do
|
23
|
+
output_map = double("output_map")
|
24
|
+
processor.should_receive(:process_all).and_return output_map
|
25
|
+
renderer.should_receive(:render).with(output_map).and_return "cfn_template"
|
26
|
+
stdout.should_receive(:puts).with "cfn_template"
|
27
|
+
cfoo.build_project
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
describe ElParser do
|
5
|
+
let(:parser) { ElParser }
|
6
|
+
it 'turns simple EL references into CloudFormation "Ref" maps' do
|
7
|
+
parser.parse("$(orange)").should == {"Ref" => "orange"}
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'turns EL references embedded in strings into appended arrays' do
|
11
|
+
parser.parse("large $(MelonType) melon").should == {"Fn::Join" => [ "" , [ "large ", { "Ref" => "MelonType" }, " melon" ]]}
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'turns multiple EL references embedded in strings into single appended arrays' do
|
15
|
+
parser.parse("I have $(number) apples and $(otherNumber) oranges").should == {"Fn::Join" => [ "" , ["I have ", { "Ref" => "number" }, " apples and ", { "Ref" => "otherNumber" }, " oranges" ]]}
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'turns EL attribute references into CloudFormation "GetAtt" maps' do
|
19
|
+
parser.parse("$(apple.color)").should == {"Fn::GetAtt" => ["apple", "color"]}
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'turns EL attribute map references into CloudFormation "GetAtt" maps' do
|
23
|
+
parser.parse("$(apple[color])").should == {"Fn::GetAtt" => ["apple", "color"]}
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'turns EL map references into CloudFormation "FindInMap" maps' do
|
27
|
+
parser.parse("$(fruit[apple][color])").should == {"Fn::FindInMap" => ["fruit", "apple", "color"]}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "doesn't expand escaped EL" do
|
31
|
+
parser.parse("\\$(apple.color) apple").should == "$(apple.color) apple"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "copes with lone backslashes" do
|
35
|
+
parser.parse("\\ apple").should == "\\ apple"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "copes with EL in maps" do
|
39
|
+
parser.parse("$(Fruit[$(AWS::FruitType)][$(FruitProperty)])").should == {"Fn::FindInMap" => ["Fruit", {"Ref" => "AWS::FruitType"}, {"Ref" => "FruitProperty"}]}
|
40
|
+
end
|
41
|
+
|
42
|
+
it "copes with EL in references" do
|
43
|
+
parser.parse("$($(appleProperty))").should == {"Ref" => {"Ref" => "appleProperty"}}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|