cfoo 0.0.1 → 0.0.2
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/README.md +90 -15
- data/TODO +3 -5
- data/bin/cfoo +2 -7
- data/features/error_reporting.feature +23 -0
- data/features/function_expansion.feature +139 -0
- data/features/step_definitions/cfoo_steps.rb +14 -2
- data/lib/cfoo.rb +4 -0
- data/lib/cfoo/array.rb +13 -0
- data/lib/cfoo/cfoo.rb +6 -2
- data/lib/cfoo/constants.rb +3 -0
- data/lib/cfoo/el_parser.rb +35 -20
- data/lib/cfoo/factory.rb +25 -0
- data/lib/cfoo/file_system.rb +20 -0
- data/lib/cfoo/module.rb +0 -4
- data/lib/cfoo/parser.rb +66 -19
- data/lib/cfoo/processor.rb +10 -10
- data/lib/cfoo/version.rb +1 -1
- data/lib/cfoo/yaml_parser.rb +2 -1
- data/spec/cfoo/array_spec.rb +23 -0
- data/spec/cfoo/cfoo_spec.rb +20 -1
- data/spec/cfoo/el_parser_spec.rb +31 -0
- data/spec/cfoo/factory_spec.rb +22 -0
- data/spec/cfoo/file_system_spec.rb +38 -0
- data/spec/cfoo/parser_spec.rb +13 -1
- data/spec/cfoo/processor_spec.rb +111 -3
- data/spec/cfoo/yaml_parser_spec.rb +5 -0
- data/spec/cfoo/yaml_spec.rb +36 -0
- metadata +146 -135
- checksums.yaml +0 -7
data/lib/cfoo/el_parser.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'parslet'
|
3
|
+
require 'cfoo/array'
|
3
4
|
|
4
5
|
module Cfoo
|
5
6
|
class ElParser < Parslet::Parser
|
@@ -16,17 +17,20 @@ module Cfoo
|
|
16
17
|
# #TODO: handle this properly
|
17
18
|
end
|
18
19
|
|
19
|
-
rule(:space) { match('\s') }
|
20
|
+
rule(:space) { match('\s').repeat(1) }
|
20
21
|
rule(:space?) { space.maybe }
|
21
22
|
rule(:escaped_dollar) { str('\$').as(:escaped_dollar) }
|
22
23
|
rule(:lone_backslash) { str('\\').as(:lone_backslash) }
|
23
|
-
rule(:lparen) { str('(') }
|
24
|
+
rule(:lparen) { str('(') >> space? }
|
24
25
|
rule(:rparen) { str(')') }
|
25
26
|
rule(:lbracket) { str('[') }
|
26
27
|
rule(:rbracket) { str(']') }
|
27
28
|
rule(:dot) { str('.') }
|
28
|
-
rule(:
|
29
|
-
rule(:
|
29
|
+
rule(:comma) { str(",") >> space? }
|
30
|
+
rule(:text_character) { match['^\\\\$'] }
|
31
|
+
rule(:identifier) { match['a-zA-Z0-9_\-:'].repeat(1).as(:identifier) >> space? }
|
32
|
+
rule(:text) { text_character.repeat(1).as(:text) }
|
33
|
+
|
30
34
|
rule(:attribute_reference) do
|
31
35
|
(
|
32
36
|
expression.as(:reference) >> (
|
@@ -42,13 +46,21 @@ module Cfoo
|
|
42
46
|
str("[") >> expression.as(:value) >> str("]")
|
43
47
|
).as(:mapping)
|
44
48
|
end
|
49
|
+
rule(:function_call) do
|
50
|
+
(
|
51
|
+
identifier.as(:function) >>
|
52
|
+
lparen >>
|
53
|
+
(expression >> (comma >> expression).repeat).as(:arguments).maybe >>
|
54
|
+
rparen
|
55
|
+
).as(:function_call)
|
56
|
+
end
|
45
57
|
rule(:reference) do
|
46
58
|
expression.as(:reference)
|
47
59
|
end
|
48
60
|
|
49
61
|
rule(:expression) { el | identifier }
|
50
62
|
rule(:el) do
|
51
|
-
str("$(") >> ( mapping | attribute_reference | reference ) >> str(")")
|
63
|
+
str("$(") >> ( mapping | attribute_reference | function_call | reference ) >> str(")")
|
52
64
|
end
|
53
65
|
rule(:string) do
|
54
66
|
( escaped_dollar | lone_backslash | el | text ).repeat.as(:string)
|
@@ -74,26 +86,29 @@ module Cfoo
|
|
74
86
|
{ "Ref" => reference }
|
75
87
|
end
|
76
88
|
|
77
|
-
rule(:mapping => { :map => subtree(:map), :key => subtree(:key), :value => subtree(:value)}) do
|
78
|
-
{ "Fn::FindInMap" => [map, key, value] }
|
79
|
-
end
|
80
|
-
|
81
89
|
rule(:attribute_reference => { :reference => subtree(:reference), :attribute => subtree(:attribute)}) do
|
82
90
|
{ "Fn::GetAtt" => [ reference, attribute ] }
|
83
91
|
end
|
84
92
|
|
85
|
-
rule(:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
93
|
+
rule(:function_call => { :function => simple(:function) }) do
|
94
|
+
{ "Fn::#{function}" => "" }
|
95
|
+
end
|
96
|
+
|
97
|
+
rule(:function_call => { :function => simple(:function), :arguments => simple(:argument) }) do
|
98
|
+
{ "Fn::#{function}" => argument }
|
99
|
+
end
|
100
|
+
|
101
|
+
rule(:function_call => { :function => simple(:function), :arguments => subtree(:arguments) }) do
|
102
|
+
{ "Fn::#{function}" => arguments }
|
103
|
+
end
|
104
|
+
|
105
|
+
rule(:mapping => { :map => subtree(:map), :key => subtree(:key), :value => subtree(:value)}) do
|
106
|
+
{ "Fn::FindInMap" => [map, key, value] }
|
107
|
+
end
|
95
108
|
|
96
|
-
|
109
|
+
rule(:string => subtree(:string_parts)) do
|
110
|
+
# EL is parsed separately from other strings
|
111
|
+
parts = string_parts.join_adjacent_strings
|
97
112
|
|
98
113
|
if parts.size == 1
|
99
114
|
parts.first
|
data/lib/cfoo/factory.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "cfoo/cfoo"
|
2
|
+
require "cfoo/file_system"
|
3
|
+
require "cfoo/parser"
|
4
|
+
require "cfoo/processor"
|
5
|
+
require "cfoo/project"
|
6
|
+
require "cfoo/renderer"
|
7
|
+
require "cfoo/yaml_parser"
|
8
|
+
|
9
|
+
module Cfoo
|
10
|
+
class Factory
|
11
|
+
def initialize(stdout, stderr)
|
12
|
+
@stdout, @stderr = stdout, stderr
|
13
|
+
end
|
14
|
+
|
15
|
+
def cfoo
|
16
|
+
yaml_parser = YamlParser.new
|
17
|
+
file_system = FileSystem.new(".", yaml_parser)
|
18
|
+
project = Project.new(file_system)
|
19
|
+
parser = Parser.new(file_system)
|
20
|
+
processor = Processor.new(parser, project)
|
21
|
+
renderer = Renderer.new
|
22
|
+
cfoo = Cfoo.new(processor, renderer, @stdout, @stderr)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/cfoo/file_system.rb
CHANGED
@@ -14,6 +14,26 @@ module Cfoo
|
|
14
14
|
@yaml_parser.load_file(resolve_file file_name)
|
15
15
|
end
|
16
16
|
|
17
|
+
def open(file_name, &block)
|
18
|
+
File.open(resolve_file(file_name), &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_coordinates(string, file_name)
|
22
|
+
matching_lines = []
|
23
|
+
open(file_name) do |file|
|
24
|
+
file.each_with_index do|line, row_index|
|
25
|
+
if line.include? string
|
26
|
+
column_index = line.index(string)
|
27
|
+
row = row_index + 1
|
28
|
+
column = column_index + 1
|
29
|
+
return [row, column]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
#TODO test this
|
34
|
+
raise "Couldn't find '#{string}' in '#{file}'"
|
35
|
+
end
|
36
|
+
|
17
37
|
def glob_relative(path)
|
18
38
|
absolute_files = Dir.glob(resolve_file path)
|
19
39
|
absolute_files.map do |file|
|
data/lib/cfoo/module.rb
CHANGED
data/lib/cfoo/parser.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
|
+
require 'cfoo/constants'
|
1
2
|
require 'cfoo/el_parser'
|
2
3
|
require 'cfoo/yaml'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'parslet'
|
6
|
+
|
7
|
+
module Parslet
|
8
|
+
class Source
|
9
|
+
def str
|
10
|
+
@str
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
3
14
|
|
4
15
|
class Object
|
5
16
|
def expand_el
|
6
|
-
raise Cfoo::Parser::
|
17
|
+
raise Cfoo::Parser::ElExpansionError, "Couldn't parse object '#{self}'. I don't know how to parse an instance of '#{self.class}'"
|
7
18
|
end
|
8
19
|
end
|
9
20
|
|
@@ -46,25 +57,55 @@ class Hash
|
|
46
57
|
end
|
47
58
|
|
48
59
|
module YAML
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
class DomainType
|
61
|
+
|
62
|
+
CLOUDFORMATION_FUNCTION_REGEX = %r[^(#{::Cfoo::CLOUDFORMATION_FUNCTIONS.join '|'})$]
|
63
|
+
|
64
|
+
def expand_el
|
65
|
+
case type_id
|
66
|
+
when "Ref"
|
67
|
+
{ "Ref" => value.expand_el }
|
68
|
+
when CLOUDFORMATION_FUNCTION_REGEX
|
69
|
+
{ "Fn::#{type_id}" => value.expand_el }
|
70
|
+
when "Concat"
|
71
|
+
{ "Fn::Join" => ['', value.expand_el] }
|
72
|
+
else
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
63
77
|
end
|
64
78
|
|
65
79
|
module Cfoo
|
66
80
|
class Parser
|
67
|
-
class
|
81
|
+
class ParseError < RuntimeError
|
82
|
+
end
|
83
|
+
|
84
|
+
class CfooParseError < ParseError
|
85
|
+
attr_reader :file_name, :cause
|
86
|
+
|
87
|
+
def initialize(file_name, failure)
|
88
|
+
super("Failed to parse '#{file_name}':\n#{failure}")
|
89
|
+
@file_name = file_name
|
90
|
+
@cause = cause
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class ElExpansionError < ParseError
|
95
|
+
end
|
96
|
+
|
97
|
+
class ElParseError < ParseError
|
98
|
+
attr_accessor :file_name, :cause, :source, :line, :column
|
99
|
+
|
100
|
+
def initialize(file_name, cause, source, line, column)
|
101
|
+
super("Failed to parse '#{file_name}':\nSource: #{source}\nLocation: #{file_name} line #{line}, column #{column} \nCause: #{cause.ascii_tree}")
|
102
|
+
|
103
|
+
@file_name = file_name
|
104
|
+
@cause = cause
|
105
|
+
@source = source
|
106
|
+
@line = line
|
107
|
+
@column = column
|
108
|
+
end
|
68
109
|
end
|
69
110
|
|
70
111
|
def initialize(file_system)
|
@@ -74,9 +115,15 @@ module Cfoo
|
|
74
115
|
def parse_file(file_name)
|
75
116
|
@file_system.parse_file(file_name).expand_el
|
76
117
|
rescue Parslet::ParseFailed => failure
|
77
|
-
|
78
|
-
|
79
|
-
|
118
|
+
#TODO: spec this somehow
|
119
|
+
cause = failure.cause
|
120
|
+
source = cause.source.str
|
121
|
+
row, column = @file_system.find_coordinates(source, file_name)
|
122
|
+
raise ElParseError.new(file_name, cause, source, row, column)
|
123
|
+
rescue ElExpansionError => failure
|
124
|
+
raise failure
|
125
|
+
rescue Exception => failure
|
126
|
+
raise CfooParseError.new(file_name, failure)
|
80
127
|
end
|
81
128
|
end
|
82
129
|
end
|
data/lib/cfoo/processor.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
|
2
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? :
|
8
|
-
our_item.concat(their_item).uniq
|
9
|
-
else
|
10
|
-
their_item
|
11
|
-
end
|
12
|
-
end
|
3
|
+
def deep_merge(other)
|
4
|
+
merge(other) do |key, our_item, their_item|
|
5
|
+
if [our_item, their_item].all? {|item| item.respond_to? :deep_merge }
|
6
|
+
our_item.deep_merge(their_item)
|
7
|
+
elsif [our_item, their_item].all? {|item| item.respond_to?(:+) && item.respond_to?(:uniq) }
|
8
|
+
our_item.concat(their_item).uniq
|
9
|
+
else
|
10
|
+
their_item
|
11
|
+
end
|
12
|
+
end
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
data/lib/cfoo/version.rb
CHANGED
data/lib/cfoo/yaml_parser.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
require 'cfoo/constants'
|
1
2
|
require 'cfoo/yaml'
|
2
3
|
|
3
4
|
module Cfoo
|
4
5
|
class YamlParser
|
5
6
|
|
6
|
-
CFN_DOMAIN_TYPES =
|
7
|
+
CFN_DOMAIN_TYPES = ::Cfoo::CLOUDFORMATION_FUNCTIONS + %w[Ref Concat]
|
7
8
|
|
8
9
|
def load_file(file_name)
|
9
10
|
#TODO: raise errors if "value" isn't the right type
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Array do
|
4
|
+
describe "#join_adjacent_strings" do
|
5
|
+
context "when the array is empty" do
|
6
|
+
it "returns an array with an empty string in it" do
|
7
|
+
[].join_adjacent_strings.should == []
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context "when array has only strings" do
|
12
|
+
it "returns an array with them all joined together" do
|
13
|
+
["j", "oi", "nme"].join_adjacent_strings.should == ["joinme"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when array has some strings and other things" do
|
18
|
+
it "returns an array with all the adjacent strings joined together" do
|
19
|
+
[["1"], "a", "bc", ["2", "3"], "d", " ", { "e" => "f"}, ""].join_adjacent_strings.should == [["1"], "abc", ["2", "3"], "d ", { "e"=> "f" }, ""]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/cfoo/cfoo_spec.rb
CHANGED
@@ -5,7 +5,8 @@ module Cfoo
|
|
5
5
|
let(:processor) { double('processor') }
|
6
6
|
let(:renderer) { double('renderer') }
|
7
7
|
let(:stdout) { double('stdout') }
|
8
|
-
let(:
|
8
|
+
let(:stderr) { double('stderr') }
|
9
|
+
let(:cfoo) { Cfoo.new(processor, renderer, stdout, stderr) }
|
9
10
|
|
10
11
|
describe "#process" do
|
11
12
|
it "processes the specified files" do
|
@@ -15,6 +16,15 @@ module Cfoo
|
|
15
16
|
stdout.should_receive(:puts).with "cfn_template"
|
16
17
|
cfoo.process("1.yml", "2.yml")
|
17
18
|
end
|
19
|
+
|
20
|
+
context "when an error is raised" do
|
21
|
+
it "prints it to stderr" do
|
22
|
+
error = RuntimeError.new("parse fail")
|
23
|
+
processor.should_receive(:process).with("1.yml").and_raise error
|
24
|
+
stderr.should_receive(:puts).with error
|
25
|
+
cfoo.process("1.yml")
|
26
|
+
end
|
27
|
+
end
|
18
28
|
end
|
19
29
|
|
20
30
|
describe "#build_project" do
|
@@ -26,6 +36,15 @@ module Cfoo
|
|
26
36
|
stdout.should_receive(:puts).with "cfn_template"
|
27
37
|
cfoo.build_project
|
28
38
|
end
|
39
|
+
|
40
|
+
context "when an error is raised" do
|
41
|
+
it "prints it to stderr" do
|
42
|
+
error = RuntimeError.new("parse fail")
|
43
|
+
processor.should_receive(:process_all).and_raise error
|
44
|
+
stderr.should_receive(:puts).with error
|
45
|
+
cfoo.build_project
|
46
|
+
end
|
47
|
+
end
|
29
48
|
end
|
30
49
|
end
|
31
50
|
end
|
data/spec/cfoo/el_parser_spec.rb
CHANGED
@@ -27,6 +27,33 @@ module Cfoo
|
|
27
27
|
parser.parse("$(fruit[apple][color])").should == {"Fn::FindInMap" => ["fruit", "apple", "color"]}
|
28
28
|
end
|
29
29
|
|
30
|
+
context "parsing function calls" do
|
31
|
+
it 'turns no-arg function calls into "Fn" maps with an empty string as the value' do
|
32
|
+
parser.parse("$(Fruit())").should == {"Fn::Fruit" => ""}
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'turns single-arg function calls into "Fn" maps with the string as the value' do
|
36
|
+
parser.parse("$(Fruit(Favorite))").should == {"Fn::Fruit" => "Favorite"}
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'turns multi-arg function calls into "Fn" maps with the an array of the arg strings as the value' do
|
40
|
+
parser.parse("$(Fruit(One, Two, Three))").should == {"Fn::Fruit" => [ "One", "Two", "Three" ]}
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'copes with no spaces between function arguments' do
|
44
|
+
parser.parse("$(Fruit(One,Two,Three))").should == {"Fn::Fruit" => [ "One", "Two", "Three" ]}
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'copes with spaces between around function arguments' do
|
48
|
+
parser.parse("$(Fruit( One , Two ,Three ))").should == {"Fn::Fruit" => [ "One", "Two", "Three" ]}
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'copes with EL as arguments' do
|
52
|
+
parser.parse("$(FindInMap(AWSRegionArch2AMI, $(AWS::Region), $(AWSInstanceType2Arch[FrontendInstanceType][Arch])))").
|
53
|
+
should == {"Fn::FindInMap" => ["AWSRegionArch2AMI", {"Ref" => "AWS::Region"}, { "Fn::FindInMap" => ["AWSInstanceType2Arch", "FrontendInstanceType", "Arch"] }]}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
30
57
|
it "doesn't expand escaped EL" do
|
31
58
|
parser.parse("\\$(apple.color) apple").should == "$(apple.color) apple"
|
32
59
|
end
|
@@ -42,6 +69,10 @@ module Cfoo
|
|
42
69
|
it "copes with EL in references" do
|
43
70
|
parser.parse("$($(appleProperty))").should == {"Ref" => {"Ref" => "appleProperty"}}
|
44
71
|
end
|
72
|
+
|
73
|
+
it "handles letters, numbers, underscores, and colons in identifiers" do
|
74
|
+
parser.parse("$(AWS::Hip_2_the_groove_identifier)").should == {"Ref" => "AWS::Hip_2_the_groove_identifier"}
|
75
|
+
end
|
45
76
|
end
|
46
77
|
end
|
47
78
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Cfoo
|
4
|
+
describe Factory do
|
5
|
+
let(:stdout) { double('stderr') }
|
6
|
+
let(:stderr) { double('stderr') }
|
7
|
+
let(:factory) { Factory.new(stdout, stderr) }
|
8
|
+
|
9
|
+
describe "#cfoo" do
|
10
|
+
it "builds a Cfoo instance with its dependencies" do
|
11
|
+
cfoo = factory.cfoo
|
12
|
+
cfoo.class.should be Cfoo
|
13
|
+
|
14
|
+
cfoo.instance_eval{ @stdout }.should be stdout
|
15
|
+
cfoo.instance_eval{ @stderr }.should be stderr
|
16
|
+
cfoo.instance_eval{ @processor }.class.should be Processor
|
17
|
+
cfoo.instance_eval{ @renderer }.class.should be Renderer
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|