Skip to content

Commit ec20021

Browse files
Merge pull request #142 from da-ar/validate_schema
(maint) Validate Type Schema
2 parents 19aa110 + 3aeb983 commit ec20021

File tree

9 files changed

+158
-137
lines changed

9 files changed

+158
-137
lines changed

.rspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
--format documentation
22
--color
3-
--order rand:123
3+
--order rand

lib/puppet/resource_api.rb

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,9 @@ class << self
1919
end
2020

2121
def register_type(definition)
22-
raise Puppet::DevError, 'requires a hash as definition, not `%{other_type}`' % { other_type: definition.class } unless definition.is_a? Hash
23-
raise Puppet::DevError, 'requires a `:name`' unless definition.key? :name
24-
raise Puppet::DevError, 'requires `:attributes`' unless definition.key? :attributes
25-
raise Puppet::DevError, '`:attributes` must be a hash, not `%{other_type}`' % { other_type: definition[:attributes].class } unless definition[:attributes].is_a?(Hash)
26-
[:title, :provider, :alias, :audit, :before, :consume, :export, :loglevel, :noop, :notify, :require, :schedule, :stage, :subscribe, :tag].each do |name|
27-
raise Puppet::DevError, 'must not define an attribute called `%{name}`' % { name: name.inspect } if definition[:attributes].key? name
28-
end
29-
if definition.key?(:title_patterns) && !definition[:title_patterns].is_a?(Array)
30-
raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class }
31-
end
32-
33-
Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition)
34-
35-
definition[:features] ||= []
36-
supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze
37-
unknown_features = definition[:features] - supported_features
38-
Puppet.warning("Unknown feature detected: #{unknown_features.inspect}") unless unknown_features.empty?
39-
40-
# fixup any weird behavior ;-)
41-
definition[:attributes].each do |name, attr|
42-
next unless attr[:behavior]
43-
if attr[:behaviour]
44-
raise Puppet::DevError, "the '#{name}' attribute has both a `behavior` and a `behaviour`, only use one"
45-
end
46-
attr[:behaviour] = attr[:behavior]
47-
attr.delete(:behavior)
48-
end
22+
# Attempt to create a TypeDefinition from the input hash
23+
# This will validate and throw if its not right
24+
type_def = TypeDefinition.new(definition)
4925

5026
# prepare the ruby module for the provider
5127
# this has to happen before Puppet::Type.newtype starts autoloading providers
@@ -57,6 +33,7 @@ def register_type(definition)
5733

5834
Puppet::Type.newtype(definition[:name].to_sym) do
5935
@docs = definition[:docs]
36+
@type_definition = type_def
6037

6138
# Keeps a copy of the provider around. Weird naming to avoid clashes with puppet's own `provider` member
6239
define_singleton_method(:my_provider) do
@@ -70,7 +47,7 @@ def my_provider
7047
end
7148

7249
define_singleton_method(:type_definition) do
73-
@type_definition ||= TypeDefinition.new(definition)
50+
@type_definition
7451
end
7552

7653
def type_definition
@@ -195,14 +172,8 @@ def to_resource
195172
# https://puppet.com/docs/puppet/6.0/custom_types.html#reference-5883
196173
# for details.
197174
send(param_or_property, name.to_sym, parent: parent) do
198-
unless options[:type]
199-
raise Puppet::DevError, "#{definition[:name]}.#{name} has no type"
200-
end
201-
202175
if options[:desc]
203176
desc "#{options[:desc]} (a #{options[:type]})"
204-
else
205-
warn("#{definition[:name]}.#{name} has no docs")
206177
end
207178

208179
# The initialize method is called when puppet core starts building up

lib/puppet/resource_api/type_definition.rb

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ class Puppet::ResourceApi::TypeDefinition
33
attr_reader :definition
44

55
def initialize(definition)
6-
raise Puppet::DevError, 'TypeDefinition requires definition to be a Hash' unless definition.is_a?(Hash)
7-
@definition = definition
6+
@data_type_cache = {}
7+
validate_schema(definition)
88
end
99

1010
def name
@@ -36,6 +36,53 @@ def feature?(feature)
3636
supported
3737
end
3838

39+
def validate_schema(definition)
40+
raise Puppet::DevError, 'Type definition must be a Hash, not `%{other_type}`' % { other_type: definition.class } unless definition.is_a?(Hash)
41+
raise Puppet::DevError, 'Type definition must have a name' unless definition.key? :name
42+
raise Puppet::DevError, 'Type definition must have `:attributes`' unless definition.key? :attributes
43+
unless definition[:attributes].is_a?(Hash)
44+
raise Puppet::DevError, '`%{name}.attributes` must be a hash, not `%{other_type}`' % {
45+
name: definition[:name], other_type: definition[:attributes].class
46+
}
47+
end
48+
[:title, :provider, :alias, :audit, :before, :consume, :export, :loglevel, :noop, :notify, :require, :schedule, :stage, :subscribe, :tag].each do |name|
49+
raise Puppet::DevError, 'must not define an attribute called `%{name}`' % { name: name.inspect } if definition[:attributes].key? name
50+
end
51+
if definition.key?(:title_patterns) && !definition[:title_patterns].is_a?(Array)
52+
raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class }
53+
end
54+
55+
Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition)
56+
57+
definition[:features] ||= []
58+
supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze
59+
unknown_features = definition[:features] - supported_features
60+
Puppet.warning("Unknown feature detected: #{unknown_features.inspect}") unless unknown_features.empty?
61+
62+
definition[:attributes].each do |key, attr|
63+
raise Puppet::DevError, "`#{definition[:name]}.#{key}` must be a Hash, not a #{attr.class}" unless attr.is_a? Hash
64+
raise Puppet::DevError, "`#{definition[:name]}.#{key}` has no type" unless attr.key? :type
65+
Puppet.warning("`#{definition[:name]}.#{key}` has no docs") unless attr.key? :desc
66+
67+
# validate the type by attempting to parse into a puppet type
68+
@data_type_cache[definition[:attributes][key][:type]] ||=
69+
Puppet::ResourceApi::DataTypeHandling.parse_puppet_type(
70+
key,
71+
definition[:attributes][key][:type],
72+
)
73+
74+
# fixup any weird behavior ;-)
75+
next unless attr[:behavior]
76+
if attr[:behaviour]
77+
raise Puppet::DevError, "the '#{key}' attribute has both a `behavior` and a `behaviour`, only use one"
78+
end
79+
attr[:behaviour] = attr[:behavior]
80+
attr.delete(:behavior)
81+
end
82+
# store the validated definition
83+
@definition = definition
84+
end
85+
3986
# validates a resource hash against its type schema
4087
def check_schema(resource)
4188
namevars.each do |namevar|
@@ -84,10 +131,7 @@ def check_schema_values(resource)
84131
bad_vals = {}
85132
resource.each do |key, value|
86133
next unless attributes[key]
87-
type = Puppet::ResourceApi::DataTypeHandling.parse_puppet_type(
88-
key,
89-
attributes[key][:type],
90-
)
134+
type = @data_type_cache[attributes[key][:type]]
91135
error_message = Puppet::ResourceApi::DataTypeHandling.try_validate(
92136
type,
93137
value,

spec/puppet/resource_api/base_context_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def send_log(log, msg)
1313
TestContext.new(definition)
1414
end
1515

16-
let(:definition) { { name: 'some_resource', attributes: { name: 'some_resource' }, features: feature_support } }
16+
let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String', desc: 'message' } }, features: feature_support } }
1717
let(:feature_support) { [] }
1818

1919
it { expect { described_class.new(nil) }.to raise_error ArgumentError, %r{BaseContext requires definition to be a Hash} }

spec/puppet/resource_api/io_context_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
RSpec.describe Puppet::ResourceApi::IOContext do
55
subject(:context) { described_class.new(definition, io) }
66

7-
let(:definition) { { name: 'some_resource' } }
7+
let(:definition) { { name: 'some_resource', attributes: {} } }
88

99
let(:io) { StringIO.new('', 'w') }
1010

spec/puppet/resource_api/puppet_context_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
RSpec.describe Puppet::ResourceApi::PuppetContext do
44
subject(:context) { described_class.new(definition) }
55

6-
let(:definition) { { name: 'some_resource' } }
6+
let(:definition) { { name: 'some_resource', attributes: {} } }
77

88
describe '#device' do
99
context 'when a NetworkDevice is configured' do

spec/puppet/resource_api/type_definition_spec.rb

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,25 @@
2323
end
2424
let(:feature_support) { [] }
2525

26-
it { expect { described_class.new(nil) }.to raise_error Puppet::DevError, %r{TypeDefinition requires definition to be a Hash} }
26+
it { expect { described_class.new(nil) }.to raise_error Puppet::DevError, %r{Type definition must be a Hash} }
2727

2828
describe '.name' do
2929
it { expect(type.name).to eq 'some_resource' }
3030
end
3131

3232
describe '#ensurable?' do
3333
context 'when type is ensurable' do
34-
let(:definition) { { attributes: { ensure: true } } }
34+
let(:definition) { { name: 'some_resource', attributes: { ensure: { type: 'Enum[absent, present]' } } } }
3535

3636
it { expect(type).to be_ensurable }
3737
it { expect(type.attributes).to be_key(:ensure) }
3838
end
3939

4040
context 'when type is not ensurable' do
41-
let(:definition) { { attributes: { string: 'something' } } }
41+
let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } } } }
4242

4343
it { expect(type).not_to be_ensurable }
44-
it { expect(type.attributes).to be_key(:string) }
44+
it { expect(type.attributes).to be_key(:name) }
4545
end
4646
end
4747

@@ -61,10 +61,9 @@
6161

6262
describe '#attributes' do
6363
context 'when type has attributes' do
64-
let(:definition) { { attributes: { string: 'test_string' } } }
64+
let(:definition) { { name: 'some_resource', attributes: { wibble: { type: 'String' } } } }
6565

66-
it { expect(type.attributes).to be_key(:string) }
67-
it { expect(type.attributes[:string]).to eq('test_string') }
66+
it { expect(type.attributes).to be_key(:wibble) }
6867
end
6968
end
7069

@@ -102,7 +101,7 @@
102101
end
103102
end
104103

105-
describe '#check_schemas' do
104+
describe '#check_schema' do
106105
context 'when resource does not contain its namevar' do
107106
let(:resource) { { nom: 'some_resource', prop: 1, ensure: 'present' } }
108107

@@ -183,4 +182,83 @@
183182
end
184183
end
185184
end
185+
186+
describe '#validate_schema' do
187+
context 'when the type definition does not have a name' do
188+
let(:definition) { { attributes: 'some_string' } }
189+
190+
it { expect { type }.to raise_error Puppet::DevError, %r{Type definition must have a name} }
191+
end
192+
193+
context 'when attributes is not a hash' do
194+
let(:definition) { { name: 'some_resource', attributes: 'some_string' } }
195+
196+
it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.attributes` must be a hash} }
197+
end
198+
199+
context 'when the schema contains title_patterns and it is not an array' do
200+
let(:definition) { { name: 'some_resource', title_patterns: {}, attributes: {} } }
201+
202+
it { expect { type }.to raise_error Puppet::DevError, %r{`:title_patterns` must be an array} }
203+
end
204+
205+
context 'when an attribute is not a hash' do
206+
let(:definition) { { name: 'some_resource', attributes: { name: 'some_string' } } }
207+
208+
it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.name` must be a Hash} }
209+
end
210+
211+
context 'when an attribute has no type' do
212+
let(:definition) { { name: 'some_resource', attributes: { name: { desc: 'message' } } } }
213+
214+
it { expect { type }.to raise_error Puppet::DevError, %r{has no type} }
215+
end
216+
217+
context 'when an attribute has no descrption' do
218+
let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } } } }
219+
220+
it 'Raises a warning message' do
221+
expect(Puppet).to receive(:warning).with('`some_resource.name` has no docs')
222+
type
223+
end
224+
end
225+
226+
context 'when an attribute has an unsupported type' do
227+
let(:definition) { { name: 'some_resource', attributes: { name: { type: 'basic' } } } }
228+
229+
it { expect { type }.to raise_error %r{<basic> is not a valid type specification} }
230+
end
231+
232+
context 'with both behavior and behaviour' do
233+
let(:definition) do
234+
{
235+
name: 'bad_behaviour',
236+
attributes: {
237+
name: {
238+
type: 'String',
239+
behaviour: :namevar,
240+
behavior: :namevar,
241+
},
242+
},
243+
}
244+
end
245+
246+
it { expect { type }.to raise_error Puppet::DevError, %r{name.*attribute has both} }
247+
end
248+
249+
context 'when registering a type with badly formed attribute type' do
250+
let(:definition) do
251+
{
252+
name: 'bad_syntax',
253+
attributes: {
254+
name: {
255+
type: 'Optional[String',
256+
},
257+
},
258+
}
259+
end
260+
261+
it { expect { type }.to raise_error Puppet::DevError, %r{The type of the `name` attribute `Optional\[String` could not be parsed:} }
262+
end
263+
end
186264
end

0 commit comments

Comments
 (0)