cfndsl 0.12.11 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,22 @@
1
+ # Global variables to adjust CfnDsl behavior
1
2
  module CfnDsl
2
- # Set global variables
3
- class Globals
4
- class << self
5
- def reserved_items
6
- %w[Resource Parameter Output].freeze
7
- end
8
- end
3
+ module_function
4
+
5
+ def disable_binding
6
+ @disable_binding = true
7
+ end
8
+
9
+ def disable_binding?
10
+ @disable_binding
11
+ end
12
+
13
+ def specification_file(file = nil)
14
+ @specification_file = file if file
15
+ @specification_file ||= File.join(ENV['HOME'], '.cfndsl/resource_specification.json')
16
+ @specification_file
17
+ end
18
+
19
+ def reserved_items
20
+ %w[Resource Parameter Output].freeze
9
21
  end
10
22
  end
@@ -28,7 +28,7 @@ module CfnDsl
28
28
  resource_name = create_resource_def(resource, info)
29
29
  parts = resource.split('::')
30
30
  until parts.empty?
31
- break if Globals.reserved_items.include? parts.first
31
+ break if CfnDsl.reserved_items.include? parts.first
32
32
  abreve_name = parts.join('_')
33
33
  if accessors.key? abreve_name
34
34
  accessors.delete abreve_name # Delete potentially ambiguous names
@@ -0,0 +1,98 @@
1
+ module CfnDsl
2
+ # Module for handling inconsistencies in the published resource specification from AWS
3
+ module Patches
4
+ # Missing/malformed resources from the resource specification
5
+ # rubocop:disable Metrics/MethodLength
6
+ def self.resources
7
+ {
8
+ 'AWS::Serverless::Function' => {
9
+ 'Properties' => {
10
+ 'Handler' => { 'PrimitiveType' => 'String' },
11
+ 'Runtime' => { 'PrimitiveType' => 'String' },
12
+ 'CodeUri' => { 'PrimitiveType' => 'String' },
13
+ 'Description' => { 'PrimitiveType' => 'String' },
14
+ 'MemorySize' => { 'PrimitiveType' => 'Integer' },
15
+ 'Timeout' => { 'PrimitiveType' => 'Integer' },
16
+ 'Environment' => { 'PrimitiveType' => 'Json' },
17
+ 'Events' => { 'PrimitiveType' => 'Json' },
18
+ 'Policies' => { 'Type' => 'List', 'ItemType' => 'Policy' }
19
+ }
20
+ },
21
+ 'AWS::Serverless::Api' => {
22
+ 'Properties' => {
23
+ 'StageName' => { 'PrimitiveType' => 'String' },
24
+ 'DefinitionUri' => { 'PrimitiveType' => 'String' },
25
+ 'CacheClusterEnabled' => { 'PrimitiveType' => 'Boolean' },
26
+ 'CacheClusterSize' => { 'PrimitiveType' => 'String' },
27
+ 'Variables' => { 'PrimitiveType' => 'Json' }
28
+ }
29
+ },
30
+ 'AWS::Serverless::SimpleTable' => {
31
+ 'Properties' => {
32
+ 'PrimaryKey' => { 'Type' => 'PrimaryKey' },
33
+ 'ProvisionedThroughput' => { 'Type' => 'ProvisionedThroughput' }
34
+ }
35
+ },
36
+ 'AWS::SSM::Parameter' => {
37
+ 'Properties' => {
38
+ 'Name' => { 'PrimitiveType' => 'String' },
39
+ 'Description' => { 'PrimitiveType' => 'String' },
40
+ 'Type' => { 'PrimitiveType' => 'String' },
41
+ 'Value' => { 'PrimitiveType' => 'String' }
42
+ }
43
+ },
44
+ 'AWS::EC2::VPNGatewayConnection' => {
45
+ 'Properties' => {
46
+ 'Type' => { 'PrimitiveType' => 'String' },
47
+ 'Tags' => { 'Type' => 'List', 'ItemType' => 'Tag' }
48
+ }
49
+ },
50
+ 'AWS::EC2::EIPAssociation' => {
51
+ 'Properties' => {
52
+ 'AllocationId' => { 'PrimitiveType' => 'String' },
53
+ 'EIP' => { 'PrimitiveType' => 'String' },
54
+ 'InstanceId' => { 'PrimitiveType' => 'String' },
55
+ 'NetworkInterfaceId' => { 'PrimitiveType' => 'String' },
56
+ 'PrivateIpAddress' => { 'PrimitiveType' => 'String' }
57
+ }
58
+ },
59
+ 'AWS::Config::ConfigurationRecorder' => {
60
+ 'Properties' => {
61
+ 'Name' => { 'PrimitiveType' => 'String' },
62
+ 'RecordingGroup' => { 'Type' => 'RecordingGroup' },
63
+ 'RoleARN' => { 'PrimitiveType' => 'String' }
64
+ }
65
+ }
66
+ }
67
+ end
68
+
69
+ # Missing/malformed types from the resource specification
70
+ def self.types
71
+ {
72
+ 'AWS::Serverless::SimpleTable.PrimaryKey' => {
73
+ 'Properties' => {
74
+ 'Name' => { 'PrimitiveType' => 'String' },
75
+ 'Type' => { 'PrimitiveType' => 'String' }
76
+ }
77
+ },
78
+ 'AWS::Serverless::SimpleTable.ProvisionedThroughput' => {
79
+ 'Properties' => {
80
+ 'ReadCapacityUnits' => { 'PrimitiveType' => 'Integer' },
81
+ 'WriteCapacityUnits' => { 'PrimitiveType' => 'Integer' }
82
+ }
83
+ },
84
+ 'AWS::Serverless::Function.Policy' => {
85
+ 'Properties' => {
86
+ 'PolicyDocument' => { 'PrimitiveType' => 'Json' },
87
+ 'PolicyName' => { 'PrimitiveType' => 'String' }
88
+ }
89
+ },
90
+ 'AWS::Cognito::IdentityPoolRoleAttachment.RulesConfigurationType' => {
91
+ 'Properties' => {
92
+ 'Rules' => { 'Type' => 'List', 'ItemType' => 'MappingRule' }
93
+ }
94
+ }
95
+ }
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,89 @@
1
+ module CfnDsl
2
+ # Helper module for bridging the gap between a static types file included in the repo
3
+ # and dynamically generating the types directly from the AWS specification
4
+ module Specification
5
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
6
+ def self.extract_resources(spec)
7
+ spec.each_with_object({}) do |(resource_name, resource_info), resources|
8
+ properties = resource_info['Properties'].each_with_object({}) do |(property_name, property_info), extracted|
9
+ # some json incorrectly labelled as Type -> Json instead of PrimitiveType
10
+ # also, AWS now has the concept of Map which cfndsl had never defined
11
+ if property_info['Type'] == 'Map' || property_info['Type'] == 'Json'
12
+ property_type = 'Json'
13
+ elsif property_info['PrimitiveType']
14
+ property_type = property_info['PrimitiveType']
15
+ elsif property_info['PrimitiveItemType']
16
+ property_type = Array(property_info['PrimitiveItemType'])
17
+ elsif property_info['ItemType']
18
+ # Tag is a reused type, but not quite primitive
19
+ # and not all resources use the general form
20
+ property_type = if property_info['ItemType'] == 'Tag'
21
+ ['Tag']
22
+ else
23
+ Array(resource_name.split('::').join + property_info['ItemType'])
24
+ end
25
+ elsif property_info['Type']
26
+ # Special types (defined below) are joined with their parent
27
+ # resource name for uniqueness and connection
28
+ property_type = resource_name.split('::').join + property_info['Type']
29
+ else
30
+ warn "could not extract type from #{resource_name}"
31
+ end
32
+ extracted[property_name] = property_type
33
+ extracted
34
+ end
35
+ resources[resource_name] = { 'Properties' => properties }
36
+ resources
37
+ end
38
+ end
39
+
40
+ def self.extract_types(spec)
41
+ primitive_types = {
42
+ 'String' => 'String',
43
+ 'Boolean' => 'Boolean',
44
+ 'Json' => 'Json',
45
+ 'Integer' => 'Integer',
46
+ 'Number' => 'Number',
47
+ 'Double' => 'Double',
48
+ 'Timestamp' => 'Timestamp',
49
+ 'Long' => 'Long'
50
+ }
51
+ spec.each_with_object(primitive_types) do |(property_name, property_info), types|
52
+ # In order to name things uniquely and allow for connections
53
+ # we extract the resource name from the property
54
+ # AWS::IAM::User.Policy becomes AWSIAMUserPolicy
55
+ root_resource = property_name.match(/(.*)\./)
56
+ root_resource_name = root_resource ? root_resource[1].gsub(/::/, '') : property_name
57
+ property_name = property_name.gsub(/::|\./, '')
58
+ next unless property_info['Properties']
59
+ properties = property_info['Properties'].each_with_object({}) do |(nested_prop_name, nested_prop_info), extracted|
60
+ if nested_prop_info['Type'] == 'Map' || nested_prop_info['Type'] == 'Json'
61
+ # The Map type and the incorrectly labelled Json type
62
+ nested_prop_type = 'Json'
63
+ elsif nested_prop_info['PrimitiveType']
64
+ nested_prop_type = nested_prop_info['PrimitiveType']
65
+ elsif nested_prop_info['PrimitiveItemType']
66
+ nested_prop_type = Array(nested_prop_info['PrimitiveItemType'])
67
+ elsif nested_prop_info['ItemType']
68
+ nested_prop_type = root_resource_name + nested_prop_info['ItemType']
69
+ elsif nested_prop_info['Type']
70
+ nested_prop_type = root_resource_name + nested_prop_info['Type']
71
+ else
72
+ warn "could not extract type from #{property_name}"
73
+ end
74
+ extracted[nested_prop_name] = nested_prop_type
75
+ extracted
76
+ end
77
+ types[property_name] = properties
78
+ types
79
+ end
80
+ end
81
+
82
+ def self.extract_from_resource_spec!
83
+ spec_file = JSON.parse File.read(CfnDsl.specification_file)
84
+ resources = extract_resources spec_file['ResourceTypes'].merge(Patches.resources)
85
+ types = extract_types spec_file['PropertyTypes'].merge(Patches.types)
86
+ { 'Resources' => resources, 'Types' => types }
87
+ end
88
+ end
89
+ end
@@ -10,9 +10,12 @@ module CfnDsl
10
10
  module Types
11
11
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
12
12
  def self.included(type_def)
13
- types_list = YAML.safe_load(File.open("#{File.dirname(__FILE__)}/#{type_def::TYPE_PREFIX}/types.yaml"))
13
+ types_list = if type_def::TYPE_PREFIX == 'aws'
14
+ Specification.extract_from_resource_spec!
15
+ else
16
+ YAML.safe_load(File.open("#{File.dirname(__FILE__)}/#{type_def::TYPE_PREFIX}/types.yaml"))
17
+ end
14
18
  type_def.const_set('Types_Internal', types_list)
15
-
16
19
  # Do a little sanity checking - all of the types referenced in Resources
17
20
  # should be represented in Types
18
21
  types_list['Resources'].keys.each do |resource_name|
@@ -1,3 +1,3 @@
1
1
  module CfnDsl
2
- VERSION = '0.12.11'.freeze
2
+ VERSION = '0.13.0'.freeze
3
3
  end
@@ -33,7 +33,7 @@ describe CfnDsl::CloudFormationTemplate do
33
33
  end
34
34
 
35
35
  it 'Serverless_Api' do
36
- template.Serverless_API(:Test) do
36
+ template.Serverless_Api(:Test) do
37
37
  StageName 'prod'
38
38
  DefinitionUri 'swagger.yml'
39
39
  CacheClusterEnabled false
@@ -89,7 +89,7 @@ describe CfnDsl::CloudFormationTemplate do
89
89
  PolicyDocument(a: 7)
90
90
  end
91
91
 
92
- expect(result2).to be_a(CfnDsl::AWS::Types::IAMEmbeddedPolicy)
92
+ expect(result2).to be_a(CfnDsl::AWS::Types::AWSIAMUserPolicy)
93
93
  expect(user.instance_variable_get('@Properties')['Policies'].value.length).to eq(2)
94
94
  end
95
95
 
@@ -13,6 +13,8 @@ describe 'cfndsl', type: :aruba do
13
13
  -D, --define "VARIABLE=VALUE" Directly set local VARIABLE as VALUE
14
14
  -v, --verbose Turn on verbose ouptut
15
15
  -b, --disable-binding Disable binding configuration
16
+ -s, --specification-file FILE Location of Cloudformation Resource Specification file
17
+ -u, --update-specification Update the Cloudformation Resource Specification file
16
18
  -h, --help Display this screen
17
19
  USAGE
18
20
  end
@@ -27,6 +29,17 @@ describe 'cfndsl', type: :aruba do
27
29
 
28
30
  before(:each) { write_file('template.rb', template_content) }
29
31
 
32
+ context 'cfndsl -u' do
33
+ it 'updates the specification file' do
34
+ run 'cfndsl -u'
35
+ expect(last_command_started).to have_output_on_stderr(<<-OUTPUT.gsub(/^ {8}/, '').chomp)
36
+ Updating specification file
37
+ Specification successfully written to #{ENV['HOME']}/.cfndsl/resource_specification.json
38
+ OUTPUT
39
+ expect(last_command_started).to have_exit_status(0)
40
+ end
41
+ end
42
+
30
43
  context 'cfndsl' do
31
44
  it 'displays the usage' do
32
45
  run 'cfndsl'
@@ -134,6 +147,7 @@ describe 'cfndsl', type: :aruba do
134
147
  it 'displays the variables as they are interpolated in the CloudFormation template' do
135
148
  run_simple 'cfndsl template.rb --yaml params.yaml --verbose'
136
149
  verbose = /
150
+ Using \s specification \s file .* \.json \n
137
151
  Loading \s YAML \s file \s .* params\.yaml \n
138
152
  Setting \s local \s variable \s DESC \s to \s yaml \n
139
153
  Loading \s template \s file \s .* template.rb \n
@@ -1 +1 @@
1
- {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"Test":{"Properties":{"StageName":"prod","DefinitionUri":"swagger.yml","CacheClusterEnabled":false,"CacheClusterSize":"512M","Variables":{"Var1":"value1"}},"Type":"AWS::Serverless::API"}}}
1
+ {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"Test":{"Properties":{"StageName":"prod","DefinitionUri":"swagger.yml","CacheClusterEnabled":false,"CacheClusterSize":"512M","Variables":{"Var1":"value1"}},"Type":"AWS::Serverless::Api"}}}
@@ -9,6 +9,9 @@ if ENV['CFNDSL_COV']
9
9
  end
10
10
  end
11
11
 
12
+ require 'cfndsl/globals'
13
+ CfnDsl.specification_file File.expand_path('../../lib/cfndsl/aws/resource_specification.json', __FILE__)
14
+ # use local fixture for tests
12
15
  require 'cfndsl'
13
16
 
14
17
  bindir = File.expand_path('../../bin', __FILE__)
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ # This is a somewhat temporary test class to compare functionality
4
+ # between the AWS, OS and new ways of defining types
5
+ RSpec.describe 'Type Definitions' do
6
+ aws_spec = YAML.load_file File.expand_path('../../lib/cfndsl/aws/types.yaml', __FILE__)
7
+ os_spec = YAML.load_file File.expand_path('../../lib/cfndsl/os/types.yaml', __FILE__)
8
+ new_spec = CfnDsl::Specification.extract_from_resource_spec!
9
+
10
+ { 'AWS' => aws_spec, 'OS' => os_spec, 'New' => new_spec }.each_pair do |cloud, specdef|
11
+ context cloud do
12
+ resources = specdef['Resources']
13
+ types = specdef['Types']
14
+
15
+ context 'Resources' do
16
+ resources.each do |name, info|
17
+ it "#{name} has all property types defined" do
18
+ properties = info['Properties']
19
+ properties.each do |_, type|
20
+ type = type.first if type.is_a?(Array)
21
+ expect(types).to have_key(type)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'Types' do
28
+ types.each do |name, type|
29
+ it "#{name} has all property types defined" do
30
+ type = type.first if type.is_a?(Array)
31
+ if type.is_a?(String)
32
+ expect(types).to have_key(type)
33
+ elsif type.is_a?(Hash)
34
+ type.values.flatten.each { |t| expect(types).to have_key(t) }
35
+ else
36
+ raise "A defined type should only be of the form String, Array or Hash, got #{type.class}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfndsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.11
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Jack
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-05-10 00:00:00.000000000 Z
12
+ date: 2017-05-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -46,13 +46,13 @@ files:
46
46
  - cfndsl.gemspec
47
47
  - lib/cfndsl.rb
48
48
  - lib/cfndsl/aws/cloud_formation_template.rb
49
+ - lib/cfndsl/aws/resource_specification.json
49
50
  - lib/cfndsl/aws/types.rb
50
51
  - lib/cfndsl/aws/types.yaml
51
52
  - lib/cfndsl/conditions.rb
52
53
  - lib/cfndsl/creation_policy.rb
53
54
  - lib/cfndsl/errors.rb
54
55
  - lib/cfndsl/external_parameters.rb
55
- - lib/cfndsl/generate_types.rb
56
56
  - lib/cfndsl/globals.rb
57
57
  - lib/cfndsl/json_serialisable_object.rb
58
58
  - lib/cfndsl/jsonable.rb
@@ -65,11 +65,13 @@ files:
65
65
  - lib/cfndsl/os/types.yaml
66
66
  - lib/cfndsl/outputs.rb
67
67
  - lib/cfndsl/parameters.rb
68
+ - lib/cfndsl/patches.rb
68
69
  - lib/cfndsl/plurals.rb
69
70
  - lib/cfndsl/properties.rb
70
71
  - lib/cfndsl/rake_task.rb
71
72
  - lib/cfndsl/ref_check.rb
72
73
  - lib/cfndsl/resources.rb
74
+ - lib/cfndsl/specification.rb
73
75
  - lib/cfndsl/types.rb
74
76
  - lib/cfndsl/update_policy.rb
75
77
  - lib/cfndsl/version.rb
@@ -114,6 +116,7 @@ files:
114
116
  - spec/spec_helper.rb
115
117
  - spec/support/shared_examples/orchestration_template.rb
116
118
  - spec/transform_spec.rb
119
+ - spec/types_definition_spec.rb
117
120
  homepage: https://github.com/stevenjack/cfndsl
118
121
  licenses:
119
122
  - MIT
@@ -165,3 +168,4 @@ test_files:
165
168
  - spec/spec_helper.rb
166
169
  - spec/support/shared_examples/orchestration_template.rb
167
170
  - spec/transform_spec.rb
171
+ - spec/types_definition_spec.rb
@@ -1,154 +0,0 @@
1
- require 'yaml'
2
- require 'cfndsl/jsonable'
3
- require 'cfndsl/plurals'
4
- require 'cfndsl/names'
5
-
6
- module CfnDsl
7
- # Type generation helper
8
- module GenerateTypes
9
- # declare classes for all of the types with named methods for setting the values
10
- class Type < JSONable
11
- end
12
-
13
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
14
- def generate_types(filename)
15
- types = YAML.safe_load(File.open(filename))
16
- const_set('Types_Internal', types)
17
-
18
- validate_types(types)
19
-
20
- classes = {}
21
-
22
- # Go through and declare all of the types first
23
- types['Types'].each_key do |typename|
24
- if !const_defined?(typename)
25
- klass = const_set(typename, Class.new(self))
26
- classes[typename] = klass
27
- else
28
- classes[typename] = const_get(typename)
29
- end
30
- end
31
-
32
- # Now go through them again and define attribute setter methods
33
- classes.each_pair do |typename, type|
34
- typeval = types['Types'][typename]
35
- next unless typeval.respond_to?(:each_pair)
36
- typeval.each_pair do |attr_name, attr_type|
37
- if attr_type.is_a?(Array)
38
- klass = const_get(attr_type[0])
39
- variable = "@#{attr_name}".to_sym
40
-
41
- method = CfnDsl::Plurals.singularize(attr_name)
42
- methods = attr_name
43
- all_methods = CfnDsl.method_names(method) + CfnDsl.method_names(methods)
44
- type.class_eval do
45
- all_methods.each do |method_name|
46
- define_method(method_name) do |value = nil, *rest, &block|
47
- existing = instance_variable_get(variable)
48
- # For no-op invocations, get out now
49
- return existing if value.nil? && rest.empty? && !block
50
-
51
- # We are going to modify the value in some
52
- # way, make sure that we have an array to mess
53
- # with if we start with nothing
54
- existing = instance_variable_set(variable, []) unless existing
55
-
56
- # special case for just a block, no args
57
- if value.nil? && rest.empty? && block
58
- val = klass.new
59
- existing.push val
60
- value.instance_eval(&block(val))
61
- return existing
62
- end
63
-
64
- # Glue all of our parameters together into
65
- # a giant array - flattening one level deep, if needed
66
- array_params = []
67
- if value.is_a?(Array)
68
- value.each { |x| array_params.push x }
69
- else
70
- array_params.push value
71
- end
72
- unless rest.empty?
73
- rest.each do |v|
74
- if v.is_a?(Array)
75
- array_params += rest
76
- else
77
- array_params.push v
78
- end
79
- end
80
- end
81
-
82
- # Here, if we were given multiple arguments either
83
- # as method [a,b,c], method(a,b,c), or even
84
- # method( a, [b], c) we end up with
85
- # array_params = [a,b,c]
86
- #
87
- # array_params will have at least one item
88
- # unless the user did something like pass in
89
- # a bunch of empty arrays.
90
- if block
91
- # TODO: Is this a bug? We don't do anything with the array conetns
92
- array_params.each do |_array_params_value|
93
- value = klass.new
94
- existing.push value
95
- value.instance_eval(&block(val)) if block
96
- end
97
- else
98
- # List of parameters with no block -
99
- # hope that the user knows what he is
100
- # doing and stuff them into our existing
101
- # array
102
- # TODO: Is this a bug? We don't do anything with the array conetns
103
- array_params.each do |_array_params_value|
104
- existing.push value
105
- end
106
- end
107
- return existing
108
- end
109
- end
110
- end
111
- else
112
- klass = const_get(attr_type)
113
- variable = "@#{attr_name}".to_sym
114
-
115
- type.class_eval do
116
- CfnDsl.method_names(attr_name) do |inner_method|
117
- define_method(inner_method) do |value = nil, *_rest, &block|
118
- value ||= klass.new
119
- instance_variable_set(variable, value)
120
- value.instance_eval(&block) if block
121
- value
122
- end
123
- end
124
- end
125
- end
126
- end
127
- end
128
- end
129
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
130
-
131
- private
132
-
133
- # Do a little sanity checking - all of the types referenced in Resources
134
- # should be represented in Types
135
- def validate_types(types)
136
- types['Resources'].values.each do |resource|
137
- resource.values.each do |thing|
138
- thing.values.flatten.each do |type|
139
- puts "unknown type #{type}" unless types['Types'].key?(type)
140
- end
141
- end
142
- end
143
-
144
- # All of the type values should also be references
145
- types['Types'].values do |type|
146
- next unless type.respond_to?(:values)
147
-
148
- type.values.each do |tv|
149
- puts "unknown type #{tv}" unless types['Types'].key?(tv)
150
- end
151
- end
152
- end
153
- end
154
- end