humidifier 2.15.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CloudFormationResourceSpecification.json +24305 -18109
- data/LICENSE +1 -1
- data/README.md +55 -33
- data/lib/humidifier/condition.rb +0 -2
- data/lib/humidifier/config.rb +46 -0
- data/lib/humidifier/fn.rb +7 -8
- data/lib/humidifier/loader.rb +37 -45
- data/lib/humidifier/mapping.rb +0 -2
- data/lib/humidifier/output.rb +4 -6
- data/lib/humidifier/parameter.rb +9 -10
- data/lib/humidifier/props.rb +177 -2
- data/lib/humidifier/resource.rb +13 -14
- data/lib/humidifier/stack.rb +166 -21
- data/lib/humidifier/version.rb +1 -1
- data/lib/humidifier.rb +31 -24
- metadata +67 -31
- data/lib/humidifier/aws_adapters/base.rb +0 -67
- data/lib/humidifier/aws_adapters/noop.rb +0 -25
- data/lib/humidifier/aws_adapters/sdkv1.rb +0 -75
- data/lib/humidifier/aws_adapters/sdkv2.rb +0 -61
- data/lib/humidifier/aws_adapters/sdkv3.rb +0 -31
- data/lib/humidifier/aws_shim.rb +0 -83
- data/lib/humidifier/configuration.rb +0 -69
- data/lib/humidifier/props/base.rb +0 -47
- data/lib/humidifier/props/boolean_prop.rb +0 -25
- data/lib/humidifier/props/double_prop.rb +0 -23
- data/lib/humidifier/props/integer_prop.rb +0 -23
- data/lib/humidifier/props/json_prop.rb +0 -31
- data/lib/humidifier/props/list_prop.rb +0 -42
- data/lib/humidifier/props/map_prop.rb +0 -46
- data/lib/humidifier/props/string_prop.rb +0 -23
- data/lib/humidifier/props/structure_prop.rb +0 -71
- data/lib/humidifier/props/timestamp_prop.rb +0 -23
- data/lib/humidifier/sdk_payload.rb +0 -122
- data/lib/humidifier/sleeper.rb +0 -25
- 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
|
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
|
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
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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.
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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,
|
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.
|
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
|
|
data/lib/humidifier/condition.rb
CHANGED
@@ -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 =
|
10
|
-
|
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
|
-
|
25
|
-
|
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
|
data/lib/humidifier/loader.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
44
|
+
private
|
61
45
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
57
|
+
def parse_spec
|
58
|
+
relative =
|
59
|
+
File.join('..', '..', 'CloudFormationResourceSpecification.json')
|
76
60
|
|
77
|
-
|
78
|
-
Humidifier.const_set(group, Module.new)
|
61
|
+
JSON.parse(File.read(File.expand_path(relative, __dir__)))
|
79
62
|
end
|
80
63
|
|
81
|
-
|
82
|
-
|
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
|
data/lib/humidifier/mapping.rb
CHANGED
@@ -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
|
data/lib/humidifier/output.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/humidifier/parameter.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
data/lib/humidifier/props.rb
CHANGED
@@ -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
|