Skip to content

Commit

Permalink
Merge 7c67898 into e3a4a31
Browse files Browse the repository at this point in the history
  • Loading branch information
kddnewton committed Dec 1, 2016
2 parents e3a4a31 + 7c67898 commit 1ccf3b6
Show file tree
Hide file tree
Showing 214 changed files with 15,298 additions and 2,175 deletions.
5 changes: 1 addition & 4 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ AllCops:
DisplayStyleGuide: true
TargetRubyVersion: 2.0
Exclude:
- 'specs/**/*'
- 'vendor/**/*'
- 'yard/**/*'

Expand All @@ -18,10 +19,6 @@ Style/EmptyLinesAroundClassBody:
Style/EmptyLinesAroundModuleBody:
Enabled: false

# Very convenient for parsing the configs
Style/FlipFlop:
Enabled: false

# Useful and concise in gsub statements
Style/PerlBackrefs:
Enabled: false
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ source 'https://rubygems.org'

gemspec

gem 'pry'
gem 'yard', git: 'https://github.com/trevorrowe/yard.git', branch: 'frameless'
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The default rake task runs the tests. Coverage is reported on the command line,

### Specs

The specs pulled from the CFN docs live under `/specs`. You can update them by running `bin/get-specs`. This script will scrape the docs by going to the [listings page](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html), finding the list of CFN resources, and then downloading the spec for each resource by going to the individual page.
The specs pulled from the CFN docs lives in `specs/CloudFormationResourceSpecification.json`. You can update it by running `bundle exec rake specs`. This script will pull down the latest [resource specification](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) to be used with Humidifier.

### Extension

Expand Down
27 changes: 27 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,31 @@ YARD::Rake::YardocTask.new do |t|
t.after = -> { FileUtils.rm(filepath) }
end

desc 'Download the latest specs from AWS'
task :specs do
require 'json'
require 'net/http'
require 'nokogiri'
require './specs/fixer'

url = URI.parse('http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html')
row =
Nokogiri::HTML(Net::HTTP.get_response(url).body).css('table[summary="Resource Specification"] tr').detect do |tr|
name_container = tr.at_css('td:first-child p')
(name_container && name_container.text.strip) == 'US East (N. Virginia)'
end

href = row.at_css('td:nth-child(2) p a').attr('href')
puts "Downloading from #{href}..."

response = Net::HTTP.get_response(URI.parse(href)).body
filepath = File.expand_path(File.join('..', 'specs', 'CloudFormationResourceSpecification.json'), __FILE__)

size = File.write(filepath, response)
puts " wrote #{filepath} (#{(size / 1024.0).round(2)}K)"

size = Fixer.new(filepath).write
puts " wrote fixed #{filepath} (#{(size / 1024.0).round(2)}K)"
end

task default: :test
35 changes: 0 additions & 35 deletions bin/get-specs

This file was deleted.

17 changes: 1 addition & 16 deletions ext/humidifier/humidifier.c
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
#include <humidifier.h>

// copies from the source string to the destination string after passing through a character whitelist filter
// note that this is exclusively built for AWS::CloudFormation::Interface, so if AWS ever fixes their docs this can
// be replaced by strcpy
static void filter(char* dest, const char* source)
{
int source_idx, dest_idx;

for (source_idx = 0, dest_idx = 0; source[source_idx] != '\0'; source_idx++) {
if (source[source_idx] != ':') {
dest[dest_idx++] = source[source_idx];
}
}
dest[dest_idx] = '\0';
}

// takes a substring from underscore_preprocess like EC2T or AWST and converts it to Ec2T or AwsT
static void format_substring(char* substr, const int substr_idx, const int capitalize)
{
Expand Down Expand Up @@ -64,7 +49,7 @@ static VALUE underscore(VALUE self, VALUE str)
char *str_value = rb_string_value_cstr(&str);
char orig_str[strlen(str_value) + 1];

filter(orig_str, str_value);
strcpy(orig_str, str_value);
preprocess(orig_str);

// manually null-terminating the string because on Fedora for strings of length 16 this breaks otherwise
Expand Down
1 change: 0 additions & 1 deletion ext/humidifier/humidifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#include <ctype.h>
#include <ruby.h>

static void filter(char* dest, const char* source);
static void format_substring(char* substr, const int substr_idx, const int capitalize);
static void preprocess(char* str);
static VALUE underscore(VALUE self, VALUE str);
Expand Down
5 changes: 5 additions & 0 deletions lib/humidifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
require 'humidifier/ref'
require 'humidifier/props'

require 'humidifier/props/base'
%w[boolean double integer json list map string structure timestamp].each do |type|
require "humidifier/props/#{type}_prop"
end

require 'humidifier/aws_shim'
require 'humidifier/condition'
require 'humidifier/configuration'
Expand Down
61 changes: 42 additions & 19 deletions lib/humidifier/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,44 @@ module Humidifier
module Config
end

# Reads each of the files under /specs/ and loads them each as a class
# Reads the specs/CloudFormationResourceSpecification.json file and load each resource as a class
class Loader
# The path to the specification file
SPECPATH = File.expand_path(
File.join('..', '..', '..', 'specs', 'CloudFormationResourceSpecification.json'),
__FILE__
)

# Handles searching the PropertyTypes specifications for a specific resource type
class StructureContainer
attr_reader :structs

def initialize(structs)
@structs = structs
end

# find the substructures necessary for the given resource key
def search(key)
results = structs.keys.grep(/#{key}/)
Hash[results.map { |result| result.gsub("#{key}.", '') }.zip(structs.values_at(*results))].merge(global)
end

private

def global
@global ||= structs.select { |key, _| !key.match(/AWS/) }
end
end

# loop through the specs and register each class
def load
spec_directory = File.expand_path(File.join('..', '..', '..', 'specs', '*'), __FILE__)
Dir[spec_directory].each { |filepath| load_from(filepath) }
parsed = JSON.parse(File.read(SPECPATH))
structs = StructureContainer.new(parsed['PropertyTypes'])

parsed['ResourceTypes'].each do |key, spec|
match = key.match(/\AAWS::(\w+)::(\w+)\z/)
register(match[1], match[2], spec, structs.search(key))
end
end

# convenience class method to build a new loader and call load
Expand All @@ -22,28 +53,20 @@ def self.load

private

def build_class(aws_name, spec)
def build_class(aws_name, spec, substructs)
Class.new(Resource) do
self.aws_name = aws_name
self.props = spec.each_with_object({}) do |spec_line, props|
prop = Props.from(spec_line)
props[prop.name] = prop unless prop.name.nil?
end
end
end

def load_from(filepath)
group, resource = Pathname.new(filepath).basename('.cf').to_s.split('-')
spec = File.readlines(filepath).select do |line|
# flipflop operator (http://stackoverflow.com/questions/14456634)
true if line.include?('Properties')...(line.strip == '}')
self.props =
Utils.enumerable_to_h(spec['Properties']) do |(key, config)|
prop = Props.from(key, config, substructs)
[prop.name, prop]
end
end
register(group, resource, spec[1..-2])
end

def register(group, resource, spec)
def register(group, resource, spec, substructs)
aws_name = "AWS::#{group}::#{resource}"
resource_class = build_class(aws_name, spec)
resource_class = build_class(aws_name, spec, substructs)

Humidifier.const_set(group, Module.new) unless Humidifier.const_defined?(group)
Humidifier.const_get(group).const_set(resource, resource_class)
Expand Down
139 changes: 13 additions & 126 deletions lib/humidifier/props.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,139 +2,26 @@ module Humidifier

# Container for property of CFN resources
module Props

# Superclass for all CFN properties
class Base

# The list of classes that are valid beyond the normal values for each prop
WHITELIST = [Fn, Ref].freeze

attr_accessor :key
attr_reader :value

def initialize(key = nil)
self.key = key
end

# true if the property type knows how to convert its values
def convertable?
respond_to?(:convert)
end

# the name of the property
def name
@name ||= Utils.underscore(key)
end

# CFN stack syntax
def to_cf(value)
[key, Serializer.dump(value)]
end

# true if the given value is of a type contained in the whitelist
def whitelisted_value?(value)
WHITELIST.any? { |clazz| value.is_a?(clazz) }
end
end

# An array property
class ArrayProp < Base
def valid?(value)
value.is_a?(Array)
end
end

# A boolean property
class BooleanProp < Base
# converts through value == 'true'
def convert(value)
if %w[true false].include?(value)
puts "WARNING: Property #{name} should be a boolean, not a string"
value == 'true'
else
value
end
end

# true if it is a boolean
def valid?(value)
value.is_a?(TrueClass) || value.is_a?(FalseClass)
end
end

# A JSON property (and the default)
class JSONProp < Base
def valid?(value)
whitelisted_value?(value) || value.is_a?(Hash) || value.is_a?(Array)
end
end

# An integer property
class IntegerProp < Base
# converts the value through #to_i unless it is whitelisted
def convert(value)
puts "WARNING: Property #{name} should be an integer" unless valid?(value)
value.to_i
end

# true if it is whitelisted or a Integer
def valid?(value)
whitelisted_value?(value) || value.is_a?(Integer)
end
end

# A string property
class StringProp < Base
# converts the value through #to_s unless it is whitelisted
def convert(value)
puts "WARNING: Property #{name} should be a string" unless valid?(value)
whitelisted_value?(value) ? value : value.to_s
end

# true if it is whitelisted or a String
def valid?(value)
whitelisted_value?(value) || value.is_a?(String)
end
end

# An number property
class NumberProp < Base
# converts the value through #to_i unless it is whitelisted
def convert(value)
puts "WARNING: Property #{name} should be a number" unless valid?(value)
value.to_i
end

# true if it is whitelisted or a Integer
def valid?(value)
whitelisted_value?(value) || value.is_a?(Integer)
end
end

class << self
# builds the appropriate prop object from the given spec line
def from(spec_line)
key, type = parse(spec_line)

case type
when 'Boolean' then BooleanProp.new(key)
when 'Integer' then IntegerProp.new(key)
when 'String' then StringProp.new(key)
when 'Number' then NumberProp.new(key)
when /\[.*?\]/ then ArrayProp.new(key)
else JSONProp.new(key)
def from(key, spec, substructs = {})
case spec['Type']
when 'List' then ListProp.new(key, spec, substructs)
when 'Map' then MapProp.new(key, spec, substructs)
else singular_from(key, spec, substructs)
end
end

# parses a spec line to return a key and type
def parse(spec_line)
spec_line.strip!
# builds a prop that is not a List or Map type
# PrimitiveType is one of Boolean, Double, Integer, Json, String, or Timestamp
def singular_from(key, spec, substructs)
primitive = spec['PrimitiveItemType'] || spec['PrimitiveType']

if spec_line.include?(':')
key, type = spec_line.gsub(/,\z/, '').split(': ').map(&:strip)
[key[1..-2], type]
if primitive
primitive = 'Integer' if primitive == 'Long'
const_get(:"#{primitive}Prop").new(key, spec)
else
[nil, spec_line]
StructureProp.new(key, spec, substructs)
end
end
end
Expand Down

0 comments on commit 1ccf3b6

Please sign in to comment.