cfoo 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.
- 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
|
+
|