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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.simplecov +4 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +676 -0
  7. data/README.md +166 -0
  8. data/Rakefile +10 -0
  9. data/TODO +29 -0
  10. data/bin/cfoo +26 -0
  11. data/cfoo.gemspec +35 -0
  12. data/features/attribute_expansion.feature +43 -0
  13. data/features/convert_yaml_to_json.feature +69 -0
  14. data/features/el_escaping.feature +21 -0
  15. data/features/map_reference_expansion.feature +68 -0
  16. data/features/modules.feature +101 -0
  17. data/features/parse_files.feature +46 -0
  18. data/features/reference_expansion.feature +49 -0
  19. data/features/step_definitions/cfoo_steps.rb +107 -0
  20. data/features/support/env.rb +5 -0
  21. data/features/yamly_shortcuts.feature +132 -0
  22. data/lib/cfoo.rb +11 -0
  23. data/lib/cfoo/cfoo.rb +15 -0
  24. data/lib/cfoo/el_parser.rb +105 -0
  25. data/lib/cfoo/file_system.rb +28 -0
  26. data/lib/cfoo/module.rb +25 -0
  27. data/lib/cfoo/parser.rb +82 -0
  28. data/lib/cfoo/processor.rb +38 -0
  29. data/lib/cfoo/project.rb +16 -0
  30. data/lib/cfoo/renderer.rb +9 -0
  31. data/lib/cfoo/version.rb +3 -0
  32. data/lib/cfoo/yaml.rb +34 -0
  33. data/lib/cfoo/yaml_parser.rb +16 -0
  34. data/spec/cfoo/cfoo_spec.rb +31 -0
  35. data/spec/cfoo/el_parser_spec.rb +47 -0
  36. data/spec/cfoo/file_system_spec.rb +61 -0
  37. data/spec/cfoo/module_spec.rb +16 -0
  38. data/spec/cfoo/parser_spec.rb +148 -0
  39. data/spec/cfoo/processor_spec.rb +52 -0
  40. data/spec/cfoo/project_spec.rb +16 -0
  41. data/spec/cfoo/renderer_spec.rb +19 -0
  42. data/spec/cfoo/yaml_parser_spec.rb +79 -0
  43. data/spec/spec_helper.rb +2 -0
  44. metadata +235 -0
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+
3
+ module Cfoo
4
+ class Renderer
5
+ def render(hash)
6
+ JSON.pretty_unparse hash
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Cfoo
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
+