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.
@@ -0,0 +1,3 @@
1
+ module Cfoo
2
+ CLOUDFORMATION_FUNCTIONS = %w[Base64 FindInMap GetAtt GetAZs Join]
3
+ end
@@ -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(:identifier) { match['a-zA-Z:'].repeat(1).as(:identifier) }
29
- rule(:text) { match['^\\\\$'].repeat(1).as(:text) }
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(: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
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
- parts.reject! {|part| part.empty? }
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
@@ -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
@@ -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|
@@ -17,9 +17,5 @@ module Cfoo
17
17
  def eql?(other)
18
18
  dir = other.dir
19
19
  end
20
-
21
- def to_s
22
- "Module[#{dir}]"
23
- end
24
20
  end
25
21
  end
@@ -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::ElParseError, "Couldn't parse object '#{self}'. I don't know how to parse an instance of '#{self.class}'"
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
- 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
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 ElParseError < RuntimeError
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
- puts "Failed to parse '#{file_name}':"
78
- puts failure.cause.ascii_tree
79
- raise failure
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
@@ -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? :concat
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
 
@@ -1,3 +1,3 @@
1
1
  module Cfoo
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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 = [ "GetAZs", "Ref", "Join", "Concat", "GetAtt", "FindInMap", "Base64" ]
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
@@ -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(:cfoo) { Cfoo.new(processor, renderer, stdout) }
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
@@ -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