humidifier 2.15.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CloudFormationResourceSpecification.json +24305 -18109
  3. data/LICENSE +1 -1
  4. data/README.md +55 -33
  5. data/lib/humidifier/condition.rb +0 -2
  6. data/lib/humidifier/config.rb +46 -0
  7. data/lib/humidifier/fn.rb +7 -8
  8. data/lib/humidifier/loader.rb +37 -45
  9. data/lib/humidifier/mapping.rb +0 -2
  10. data/lib/humidifier/output.rb +4 -6
  11. data/lib/humidifier/parameter.rb +9 -10
  12. data/lib/humidifier/props.rb +177 -2
  13. data/lib/humidifier/resource.rb +13 -14
  14. data/lib/humidifier/stack.rb +166 -21
  15. data/lib/humidifier/version.rb +1 -1
  16. data/lib/humidifier.rb +31 -24
  17. metadata +67 -31
  18. data/lib/humidifier/aws_adapters/base.rb +0 -67
  19. data/lib/humidifier/aws_adapters/noop.rb +0 -25
  20. data/lib/humidifier/aws_adapters/sdkv1.rb +0 -75
  21. data/lib/humidifier/aws_adapters/sdkv2.rb +0 -61
  22. data/lib/humidifier/aws_adapters/sdkv3.rb +0 -31
  23. data/lib/humidifier/aws_shim.rb +0 -83
  24. data/lib/humidifier/configuration.rb +0 -69
  25. data/lib/humidifier/props/base.rb +0 -47
  26. data/lib/humidifier/props/boolean_prop.rb +0 -25
  27. data/lib/humidifier/props/double_prop.rb +0 -23
  28. data/lib/humidifier/props/integer_prop.rb +0 -23
  29. data/lib/humidifier/props/json_prop.rb +0 -31
  30. data/lib/humidifier/props/list_prop.rb +0 -42
  31. data/lib/humidifier/props/map_prop.rb +0 -46
  32. data/lib/humidifier/props/string_prop.rb +0 -23
  33. data/lib/humidifier/props/structure_prop.rb +0 -71
  34. data/lib/humidifier/props/timestamp_prop.rb +0 -23
  35. data/lib/humidifier/sdk_payload.rb +0 -122
  36. data/lib/humidifier/sleeper.rb +0 -25
  37. data/lib/humidifier/utils.rb +0 -19
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2016 Localytics http://www.localytics.com
3
+ Copyright (c) 2016-2019 Localytics http://www.localytics.com
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -7,48 +7,70 @@ Humidifier allows you to build AWS CloudFormation (CFN) templates programmatical
7
7
 
8
8
  For the full docs, go to [https://localytics.github.io/humidifier/](http://localytics.github.io/humidifier/). For local development instructions, see the [Development](https://localytics.github.io/humidifier/#label-Development) section.
9
9
 
10
- This project does *not* follow semantic versioning, but instead is linked to AWS' CloudFormation resource specification version since `1.2.1`. If there are developmental changes unrelated to bumping the resource specification, then they are released as another number incremented on the end of the resource specification version. As a result, breaking changes are reserved for whenever AWS updates with minor version changes.
10
+ This project follows semantic versioning linked to AWS' CloudFormation resource specification version since `1.2.1`.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'humidifier'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install humidifier
11
27
 
12
28
  ## Getting started
13
29
 
14
- Stacks are represented by the `Humidifier::Stack` class. You can set any of the top-level JSON attributes through the initializer. Resources are represented by an exact mapping from `AWS` resource names to `Humidifier` resources names (e.g. `AWS::EC2::Instance` becomes `Humidifier::EC2::Instance`). Resources have accessors for each JSON attribute. Each attribute can also be set through the `initialize`, `update`, and `update_attribute` methods.
30
+ Stacks are represented by the `Humidifier::Stack` class. You can set any of the top-level JSON attributes (such as `name` and `description`) through the initializer.
31
+
32
+ Resources are represented by an exact mapping from `AWS` resource names to `Humidifier` resources names (e.g. `AWS::EC2::Instance` becomes `Humidifier::EC2::Instance`). Resources have accessors for each JSON attribute. Each attribute can also be set through the `initialize`, `update`, and `update_attribute` methods.
15
33
 
16
34
  ### Example usage
17
35
 
18
- ```ruby
19
- stack = Humidifier::Stack.new(name: 'Example-Stack', aws_template_format_version: '2010-09-09')
36
+ The below example will create a stack with two resources, a loader balancer and an auto scaling group. It then deploys the new stack and pauses execution until the stack is finished being created.
20
37
 
21
- load_balancer = Humidifier::ElasticLoadBalancing::LoadBalancer.new(
22
- listeners: [{ load_balancer_port: 80, protocol: 'http', instance_port: 80, instance_protocol: 'http' }]
38
+ ```ruby
39
+ stack = Humidifier::Stack.new(name: 'Example-Stack')
40
+
41
+ stack.add(
42
+ 'LoaderBalancer',
43
+ Humidifier::ElasticLoadBalancing::LoadBalancer.new(
44
+ scheme: 'internal',
45
+ listeners: [
46
+ {
47
+ load_balancer_port: 80,
48
+ protocol: 'http',
49
+ instance_port: 80,
50
+ instance_protocol: 'http'
51
+ }
52
+ ]
53
+ )
23
54
  )
24
- load_balancer.scheme = 'internal'
25
55
 
26
- auto_scaling_group = Humidifier::AutoScaling::AutoScalingGroup.new(min_size: '1', max_size: '20')
27
- auto_scaling_group.update(
28
- availability_zones: ['us-east-1a'],
29
- load_balancer_names: [Humidifier.ref('LoadBalancer')]
56
+ stack.add(
57
+ 'AutoScalingGroup',
58
+ Humidifier::AutoScaling::AutoScalingGroup.new(
59
+ min_size: '1',
60
+ max_size: '20',
61
+ availability_zones: ['us-east-1a'],
62
+ load_balancer_names: [Humidifier.ref('LoadBalancer')]
63
+ )
30
64
  )
31
65
 
32
- stack.add('LoadBalancer', load_balancer)
33
- stack.add('AutoScalingGroup', auto_scaling_group)
34
66
  stack.deploy_and_wait
35
67
  ```
36
68
 
37
69
  ### Interfacing with AWS
38
70
 
39
- Once stacks have the appropriate resources, you can query AWS to handle all stack CRUD operations. The operations themselves are intuitively named (i.e. `create`, `update`, `delete`). There are also convenience methods for validating a stack body (`valid?`), checking the existence of a stack (`exists?`), and creating or updating based on existence (`deploy`). The `create`, `update`, `delete`, and `deploy` methods all have `_and_wait` corollaries that will cause the main ruby thread to sleep until the operation is complete.
40
-
41
- #### SDK version
42
-
43
- Humidifier assumes you have an `aws-sdk` gem installed when you call these operations. It detects the version of the gem you have installed and uses the appropriate API depending on what is available. If Humidifier cannot find any way to use the AWS SDK, it will warn you on every API call and simply return false.
44
-
45
- You can also manually specify the version of the SDK that you want to use, in the case that you have both gems in your load path. In that case, you would specify it on the Humidifier configuration object:
71
+ Once stacks have the appropriate resources, you can query AWS to handle all stack CRUD operations. The operations themselves are intuitively named (i.e. `#create`, `#update`, `#delete`). There are also convenience methods for validating a stack body (`#valid?`), checking the existence of a stack (`#exists?`), and creating or updating based on existence (`#deploy`).
46
72
 
47
- ```ruby
48
- Humidifier.configure do |config|
49
- config.sdk_version = 1
50
- end
51
- ```
73
+ There are additionally four functions on `Humidifier::Stack` that support waiting for execution in AWS to finish. They all have non-blocking corollaries, and are named after them. They are: `#create_and_wait`, `#update_and_wait`, `#delete_and_wait`, and `#deploy_and_wait`.
52
74
 
53
75
  #### CloudFormation functions
54
76
 
@@ -56,19 +78,19 @@ You can use CFN intrinsic functions and references using `Humidifier.fn.[name]`
56
78
 
57
79
  #### Change Sets
58
80
 
59
- Instead of immediately pushing your changes to CloudFormation, Humidifier also supports change sets. Change sets are a powerful feature that allow you to see the changes that will be made before you make them. To read more about change sets see the [announcement article](https://aws.amazon.com/blogs/aws/new-change-sets-for-aws-cloudformation/). To use them in Humidifier, `Stack` has the `create_change_set` and `deploy_change_set` methods. The `create_change_set` method will create a change set on the stack. The `deploy_change_set` method will create a change set if the stack currently exists, and otherwise will create the stack.
81
+ Instead of immediately pushing your changes to CloudFormation, Humidifier also supports change sets. Change sets are a powerful feature that allow you to see the changes that will be made before you make them. To read more about change sets see the [announcement article](https://aws.amazon.com/blogs/aws/new-change-sets-for-aws-cloudformation/). To use them in Humidifier, `Humidifier::Stack` has the `#create_change_set` and `#deploy_change_set` methods. The `#create_change_set` method will create a change set on the stack. The `#deploy_change_set` method will create a change set if the stack currently exists, and otherwise will create the stack.
60
82
 
61
83
  ### Introspection
62
84
 
63
- To see the template body, you can check the `to_cf` method on stacks, resources, fns, and refs. All of them will output a hash of what will be uploaded (except the stack, which will output a string representation).
85
+ To see the template body, you can check the `#to_cf` method on stacks, resources, fns, and refs. All of them will output a hash of what will be uploaded (except the stack, which will output a string representation).
64
86
 
65
- Humidifier itself contains a registry of all possible resources that it supports. You can access it with `Humidifier.registry` which is a hash of AWS resource name pointing to the class.
87
+ Humidifier itself contains a registry of all possible resources that it supports. You can access it with `Humidifier::registry` which is a hash of AWS resource name pointing to the class.
66
88
 
67
- Resources have an `aws_name` method to see how AWS references them. They also contain a `props` method that contains a hash of the name that Humidifier uses to reference the prop pointing to the appropriate prop object.
89
+ Resources have an `::aws_name` method to see how AWS references them. They also contain a `::props` method that contains a hash of the name that Humidifier uses to reference the prop pointing to the appropriate prop object.
68
90
 
69
91
  ### Large templates
70
92
 
71
- When templates are especially large (larger than 51,200 bytes), they cannot be uploaded directly through the AWS SDK. You can configure Humidifier to seamlessly upload the templates to S3 and reference them using an S3 URL instead by:
93
+ When templates are especially large (larger than 51,200 bytes), they cannot be uploaded directly through the AWS SDK. You can configure `Humidifier` to seamlessly upload the templates to S3 and reference them using an S3 URL instead by:
72
94
 
73
95
  ```ruby
74
96
  Humidifier.configure do |config|
@@ -80,13 +102,13 @@ end
80
102
  ### Forcing uploading
81
103
 
82
104
  You can force a stack to upload its template to S3 regardless of the size of the template. This is a useful option if you're going to be deploying multiple
83
- copies of a template or you just generally want a backup. You can set this option on a per-stack basis:
105
+ copies of a template or if you want a backup. You can set this option on a per-stack basis:
84
106
 
85
107
  ```ruby
86
108
  stack.deploy(force_upload: true)
87
109
  ```
88
110
 
89
- or globally, but setting the configuration option:
111
+ or globally, by setting the configuration option:
90
112
 
91
113
  ```ruby
92
114
  Humidifier.configure do |config|
@@ -96,7 +118,7 @@ end
96
118
 
97
119
  ## Development
98
120
 
99
- To get started, ensure you have ruby installed, version 2.1 or later. From there, install the `bundler` gem: `gem install bundler` and then `bundle install` in the root of the repository.
121
+ To get started, ensure you have ruby installed, version 2.4 or later. From there, install the `bundler` gem: `gem install bundler` and then `bundle install` in the root of the repository.
100
122
 
101
123
  ### Testing
102
124
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Represents a CFN stack condition
5
4
  class Condition
6
5
  attr_reader :opts
7
6
 
@@ -9,7 +8,6 @@ module Humidifier
9
8
  @opts = opts
10
9
  end
11
10
 
12
- # CFN stack syntax
13
11
  def to_cf
14
12
  Serializer.dump(opts)
15
13
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Humidifier
4
+ # A container for user params
5
+ class Config
6
+ # If true, always upload the CloudFormation template to the configured S3
7
+ # destination. A useful option if you're going to be deploying multiple
8
+ # copies of a template or you just generally want a backup.
9
+ attr_accessor :force_upload
10
+
11
+ # The S3 bucket to which to deploy CloudFormation templates when
12
+ # `always_upload` is set to true or the template is too big for a string
13
+ # literal.
14
+ attr_accessor :s3_bucket
15
+
16
+ # An optional prefix for the JSON file names.
17
+ attr_accessor :s3_prefix
18
+
19
+ def initialize(opts = {})
20
+ @force_upload = opts[:force_upload]
21
+ @s3_bucket = opts[:s3_bucket]
22
+ @s3_prefix = opts[:s3_prefix]
23
+ end
24
+
25
+ # raise an error unless the s3_bucket field is set
26
+ # rubocop:disable Metrics/MethodLength
27
+ def ensure_upload_configured!(identifier)
28
+ return if s3_bucket
29
+
30
+ upload_message = <<~MSG
31
+ The %<identifier>s stack's body is too large to be use the template_body
32
+ option, and therefore must use the template_url option instead. You can
33
+ configure Humidifier to do this automatically by setting up the s3
34
+ config on the top-level Humidifier object like so:
35
+
36
+ Humidifier.configure do |config|
37
+ config.s3_bucket = 'my.s3.bucket'
38
+ config.s3_prefix = 'my-prefix/' # optional
39
+ end
40
+ MSG
41
+
42
+ raise upload_message.gsub('%<identifier>s', identifier)
43
+ end
44
+ # rubocop:enable Metrics/MethodLength
45
+ end
46
+ end
data/lib/humidifier/fn.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Builds CFN function calls
5
4
  class Fn
6
5
  # The list of all internal functions provided by AWS from
7
6
  # http://docs.aws.amazon.com
8
7
  # /AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
9
- FUNCTIONS = Utils.underscored(%w[And Base64 Equals FindInMap GetAtt GetAZs
10
- If ImportValue Join Not Or Select Sub])
8
+ FUNCTIONS =
9
+ Humidifier.underscore(
10
+ %w[And Base64 Cidr Equals FindInMap GetAtt GetAZs If ImportValue Join
11
+ Not Or Select Split Sub Transform]
12
+ )
11
13
 
12
14
  attr_reader :name, :value
13
15
 
@@ -16,15 +18,12 @@ module Humidifier
16
18
  @value = value
17
19
  end
18
20
 
19
- # CFN stack syntax
20
21
  def to_cf
21
22
  { name => Serializer.dump(value) }
22
23
  end
23
24
 
24
- class << self
25
- FUNCTIONS.each do |name, func|
26
- define_method(func) { |value| new(name, value) }
27
- end
25
+ FUNCTIONS.each do |name, func|
26
+ define_singleton_method(func) { |value| new(name, value) }
28
27
  end
29
28
  end
30
29
  end
@@ -1,25 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Pre-setting this module because AWS has a "Config" module and the below
5
- # register method dynamically looks up the module to see whether or not it
6
- # exists, which before ruby 2.2 would result in the warning:
7
- # `const_defined?': Use RbConfig instead of obsolete and deprecated Config.
8
- # @aws AWS::Config
9
- module Config
10
- end
11
-
12
4
  # Reads the specs/CloudFormationResourceSpecification.json file and load each
13
5
  # resource as a class
14
- class Loader
15
- filename = 'CloudFormationResourceSpecification.json'
16
-
17
- # The path to the specification file
18
- SPECPATH = File.expand_path(File.join('..', '..', '..', filename), __FILE__)
19
-
6
+ module Loader
20
7
  # Handles searching the PropertyTypes specifications for a specific
21
8
  # resource type
22
- class StructureContainer
9
+ class PropertyTypes
23
10
  attr_reader :structs
24
11
 
25
12
  def initialize(structs)
@@ -40,46 +27,51 @@ module Humidifier
40
27
  end
41
28
  end
42
29
 
43
- # loop through the specs and register each class
44
- def load
45
- parsed = JSON.parse(File.read(SPECPATH))
46
- structs = StructureContainer.new(parsed['PropertyTypes'])
30
+ class << self
31
+ # loop through the specs and register each class
32
+ def load
33
+ parsed = parse_spec
34
+ types = PropertyTypes.new(parsed['PropertyTypes'])
47
35
 
48
- parsed['ResourceTypes'].each do |key, spec|
49
- match = key.match(/\A(\w+)::(\w+)::(\w+)\z/)
36
+ parsed['ResourceTypes'].each do |key, spec|
37
+ match = key.match(/\A(\w+)::(\w+)::(\w+)\z/)
38
+ register(match[1], match[2], match[3], spec, types.search(key))
39
+ end
50
40
 
51
- register(match[1], match[2], match[3], spec, structs.search(key))
41
+ Humidifier.registry.freeze
52
42
  end
53
- end
54
-
55
- # convenience class method to build a new loader and call load
56
- def self.load
57
- new.load
58
- end
59
43
 
60
- private
44
+ private
61
45
 
62
- def build_class(aws_name, spec, substructs)
63
- Class.new(Resource) do
64
- self.aws_name = aws_name
65
- self.props =
66
- spec['Properties'].map do |(key, config)|
67
- prop = Props.from(key, config, substructs)
68
- [prop.name, prop]
69
- end.to_h
46
+ def build_class(aws_name, spec, substructs)
47
+ Class.new(Resource) do
48
+ self.aws_name = aws_name
49
+ self.props =
50
+ spec['Properties'].map do |(key, config)|
51
+ prop = Props.from(key, config, substructs)
52
+ [prop.name, prop]
53
+ end.to_h
54
+ end
70
55
  end
71
- end
72
56
 
73
- def register(top, group, resource, spec, substructs)
74
- aws_name = "#{top}::#{group}::#{resource}"
75
- resource_class = build_class(aws_name, spec, substructs)
57
+ def parse_spec
58
+ relative =
59
+ File.join('..', '..', 'CloudFormationResourceSpecification.json')
76
60
 
77
- unless Humidifier.const_defined?(group)
78
- Humidifier.const_set(group, Module.new)
61
+ JSON.parse(File.read(File.expand_path(relative, __dir__)))
79
62
  end
80
63
 
81
- Humidifier.const_get(group).const_set(resource, resource_class)
82
- Humidifier.registry[aws_name] = resource_class
64
+ def register(top, group, resource, spec, substructs)
65
+ aws_name = "#{top}::#{group}::#{resource}"
66
+ resource_class = build_class(aws_name, spec, substructs)
67
+
68
+ unless Humidifier.const_defined?(group)
69
+ Humidifier.const_set(group, Module.new)
70
+ end
71
+
72
+ Humidifier.const_get(group).const_set(resource, resource_class)
73
+ Humidifier.registry[aws_name] = resource_class
74
+ end
83
75
  end
84
76
  end
85
77
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Represents a CFN stack mapping
5
4
  class Mapping
6
5
  attr_reader :opts
7
6
 
@@ -9,7 +8,6 @@ module Humidifier
9
8
  @opts = opts
10
9
  end
11
10
 
12
- # CFN stack syntax
13
11
  def to_cf
14
12
  Serializer.dump(opts)
15
13
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Represents a CFN stack output
5
4
  class Output
6
5
  attr_reader :description, :value, :export_name
7
6
 
@@ -11,12 +10,11 @@ module Humidifier
11
10
  @export_name = opts[:export_name]
12
11
  end
13
12
 
14
- # CFN stack syntax
15
13
  def to_cf
16
- cf = { 'Value' => Serializer.dump(value) }
17
- cf['Description'] = description if description
18
- cf['Export'] = { 'Name' => export_name } if export_name
19
- cf
14
+ { 'Value' => Serializer.dump(value) }.tap do |cf|
15
+ cf['Description'] = description if description
16
+ cf['Export'] = { 'Name' => export_name } if export_name
17
+ end
20
18
  end
21
19
  end
22
20
  end
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Represents a CFN stack parameter
5
4
  class Parameter
6
- # The allowed properties of all stack parameters
7
5
  PROPERTIES =
8
- Utils.underscored(%w[AllowedPattern AllowedValues ConstraintDescription
9
- Default Description MaxLength MaxValue MinLength
10
- MinValue NoEcho])
6
+ Humidifier.underscore(
7
+ %w[AllowedPattern AllowedValues ConstraintDescription Default
8
+ Description MaxLength MaxValue MinLength MinValue NoEcho]
9
+ )
11
10
 
12
11
  attr_reader :type, *PROPERTIES.values
13
12
 
@@ -21,12 +20,12 @@ module Humidifier
21
20
 
22
21
  # CFN stack syntax
23
22
  def to_cf
24
- cf = { 'Type' => type }
25
- PROPERTIES.each do |name, prop|
26
- val = send(prop)
27
- cf[name] = Serializer.dump(val) if val
23
+ { 'Type' => type }.tap do |cf|
24
+ PROPERTIES.each do |name, prop|
25
+ value = public_send(prop)
26
+ cf[name] = Serializer.dump(value) if value
27
+ end
28
28
  end
29
- cf
30
29
  end
31
30
  end
32
31
  end
@@ -1,8 +1,183 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Humidifier
4
- # Container for property of CFN resources
5
4
  module Props
5
+ # Superclass for all CFN properties
6
+ class Prop
7
+ # The list of classes that are valid beyond the normal values for each
8
+ # prop
9
+ WHITELIST = [Fn, Ref].freeze
10
+
11
+ attr_reader :key, :name, :spec
12
+
13
+ def initialize(key, spec = {})
14
+ @key = key
15
+ @name = key.underscore
16
+ @spec = spec
17
+ end
18
+
19
+ # the link to the AWS docs
20
+ def documentation
21
+ spec['Documentation']
22
+ end
23
+
24
+ # true if this property is required by the resource
25
+ def required?
26
+ spec['Required']
27
+ end
28
+
29
+ # CFN stack syntax
30
+ def to_cf(value)
31
+ [key, Serializer.dump(value)]
32
+ end
33
+
34
+ # the type of update that occurs when this property is updated on its
35
+ # associated resource
36
+ def update_type
37
+ spec['UpdateType']
38
+ end
39
+
40
+ def valid?(value)
41
+ self.class.allowed_types.any? { |type| value.is_a?(type) }
42
+ end
43
+
44
+ class << self
45
+ def allowed_types
46
+ @allowed_types ||= [Fn, Ref]
47
+ end
48
+
49
+ def allow_type(*types)
50
+ allowed_types
51
+ @allowed_types += types
52
+ end
53
+ end
54
+ end
55
+
56
+ class BooleanProp < Prop
57
+ allow_type TrueClass, FalseClass
58
+ end
59
+
60
+ class DoubleProp < Prop
61
+ allow_type Integer, Float
62
+ end
63
+
64
+ class IntegerProp < Prop
65
+ allow_type Integer
66
+ end
67
+
68
+ class JsonProp < Prop
69
+ allow_type Hash
70
+ end
71
+
72
+ class StringProp < Prop
73
+ allow_type String
74
+ end
75
+
76
+ class TimestampProp < Prop
77
+ allow_type Time, Date
78
+ end
79
+
80
+ class ListProp < Prop
81
+ attr_reader :subprop
82
+
83
+ def initialize(key, spec = {}, substructs = {})
84
+ super(key, spec)
85
+ @subprop = Props.singular_from(key, spec, substructs)
86
+ end
87
+
88
+ def to_cf(list)
89
+ cf_value =
90
+ if list.respond_to?(:to_cf)
91
+ list.to_cf
92
+ else
93
+ list.map { |value| subprop.to_cf(value).last }
94
+ end
95
+
96
+ [key, cf_value]
97
+ end
98
+
99
+ def valid?(list)
100
+ return true if super(list)
101
+
102
+ list.is_a?(Enumerable) && list.all? { |value| subprop.valid?(value) }
103
+ end
104
+ end
105
+
106
+ class MapProp < Prop
107
+ attr_reader :subprop
108
+
109
+ def initialize(key, spec = {}, substructs = {})
110
+ super(key, spec)
111
+ @subprop = Props.singular_from(key, spec, substructs)
112
+ end
113
+
114
+ def to_cf(map)
115
+ cf_value =
116
+ if map.respond_to?(:to_cf)
117
+ map.to_cf
118
+ else
119
+ map.map do |subkey, subvalue|
120
+ [subkey, subprop.to_cf(subvalue).last]
121
+ end.to_h
122
+ end
123
+
124
+ [key, cf_value]
125
+ end
126
+
127
+ def valid?(map)
128
+ return true if super(map)
129
+
130
+ map.is_a?(Hash) && map.values.all? { |value| subprop.valid?(value) }
131
+ end
132
+ end
133
+
134
+ class StructureProp < Prop
135
+ attr_reader :subprops
136
+
137
+ def initialize(key, spec = {}, substructs = {})
138
+ super(key, spec)
139
+ @subprops = subprops_from(substructs, spec['ItemType'] || spec['Type'])
140
+ end
141
+
142
+ def to_cf(struct)
143
+ cf_value =
144
+ if struct.respond_to?(:to_cf)
145
+ struct.to_cf
146
+ else
147
+ struct.map do |subkey, subvalue|
148
+ subprops[subkey.to_s].to_cf(subvalue)
149
+ end.to_h
150
+ end
151
+
152
+ [key, cf_value]
153
+ end
154
+
155
+ def valid?(struct)
156
+ super(struct) || (struct.is_a?(Hash) && valid_struct?(struct))
157
+ end
158
+
159
+ private
160
+
161
+ def subprops_from(substructs, type)
162
+ subprop_names = substructs.fetch(type, {}).fetch('Properties', {})
163
+
164
+ subprop_names.each_with_object({}) do |(key, config), subprops|
165
+ subprops[key.underscore] =
166
+ if config['ItemType'] == type
167
+ self
168
+ else
169
+ Props.from(key, config, substructs)
170
+ end
171
+ end
172
+ end
173
+
174
+ def valid_struct?(struct)
175
+ struct.all? do |key, value|
176
+ subprops.key?(key.to_s) && subprops[key.to_s].valid?(value)
177
+ end
178
+ end
179
+ end
180
+
6
181
  class << self
7
182
  # builds the appropriate prop object from the given spec line
8
183
  def from(key, spec, substructs = {})
@@ -19,7 +194,7 @@ module Humidifier
19
194
  def singular_from(key, spec, substructs)
20
195
  primitive = spec['PrimitiveItemType'] || spec['PrimitiveType']
21
196
 
22
- if primitive
197
+ if primitive && !%w[List Map].include?(primitive)
23
198
  primitive = 'Integer' if primitive == 'Long'
24
199
  const_get(:"#{primitive}Prop").new(key, spec)
25
200
  else