Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
122 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,23 +5,55 @@ Status: Draft | |
Type: Standards Track | ||
--- | ||
|
||
# Resource Properties | ||
# Resource Attribute Improvements | ||
|
||
We add `property` DSL to resources, similar to (and interoperable with) LWRP `attribute`. | ||
We add a number of enhancements to `attribute`: | ||
|
||
It works very similarly to attribute syntax. There are no backwards compatibility issues with this proposal, as all `attribute` functionality remains the same. | ||
- Make nil a valid value (`path nil`) | ||
- Make a nicer type / validation syntax (`property :path, String`) | ||
- Add lazy defaults. | ||
- Add coercion. | ||
- Add `property` (an alias to `attribute`) to `Chef::Resource` to make it available to all users. | ||
- Add `property_is_set?(:property_name)` | ||
|
||
## Motivation | ||
|
||
As a Chef user, | ||
I want to be able to use natural syntax for properties, | ||
So that I can spend less time writing cookbooks, and make them more readable. | ||
|
||
As a Chef user, | ||
I want resources to be more readable and ubiquitous, | ||
So that I can easily tell the interface to things I use. | ||
|
||
As a Chef user, | ||
I want resource attributes and node attributes to use different words, | ||
so that they don't lead me to conflate them as concepts. | ||
|
||
## Specification | ||
|
||
### Defining properties | ||
### Make nil a valid value | ||
|
||
It will now be possible to set a property to `nil` by saying `my_property nil`. (Currently, this will not change the value of `my_property`.) | ||
|
||
In Chef 12, we will keep behavior the same, and *deprecate* setting a property to `nil`. We will allow properties to explicitly allow `nil`, however, by specifying it explicitly as a valid value: `property :path, [ String, nil ]`. | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
### Make defaults lazy | ||
|
||
`lazy` defaults are automatically run in the context of the *instance*: | ||
|
||
```ruby | ||
class MysqlInstall < Chef::Resource | ||
property :root_path, String, default: '/' | ||
property :config_path, String, default: lazy { File.join(root_path, 'config') } | ||
end | ||
``` | ||
|
||
This is a breaking change, and in Chef 12 will only affect properties (not attributes). | ||
|
||
Resource class definitions may now call `property` to create a resource property. This works similarly to LWRP attribute, but with some important additions and differences. | ||
### `property` | ||
This comment has been minimized.
Sorry, something went wrong.
coderanger
Contributor
|
||
|
||
`property` will be added to `Chef::Resource` as the primary way to write properties. This is to alleviate confusion around resource and node attributes. | ||
|
||
```ruby | ||
class MyResource < Chef::Resource | ||
|
@@ -40,166 +72,151 @@ When a property is defined this way: | |
- `MyResource.properties` will contain the property type. | ||
- The setter and getter manipulate the class variable `@<name>`. | ||
|
||
#### Use `property` instead of `attribute` in documentation | ||
(This is the same as before, except the addition of the `properties` hash.) | ||
|
||
`attribute` will continue to be supported; there are simply too many things in the world to deprecate it. However, any generic documentation that talks about attributes will be renamed to talk about properties. `attribute` itself will still be documented. | ||
#### `attribute` | ||
|
||
`attribute` will remain on LWRPs, and be an alias to `property` with no distinctions. | ||
|
||
### Setting properties | ||
#### Use `property` instead of `attribute` in documentation | ||
|
||
### nil | ||
`attribute` will continue to be supported; there are simply too many things in the world to deprecate it. However, any generic documentation that talks about attributes will be renamed to talk about properties. `attribute` itself will still be documented. | ||
|
||
In order to allow for `nil` as a potential explicit value, property setters accept `nil` and set the value of the property to `nil`. This differs from `attribute`, which considers `property(nil)` to be a get. | ||
We will need to write a comprehensive resource writing guide as well, in order to get it pumped to the top of Google, so that `attribute` comes up less and less often in searches and `property` comes up more and more. | ||
|
||
#### lazy values | ||
### Property type | ||
|
||
Properties may be set to lazy values, which work the same as in attributes: they are treated as computed values, popped open and validated each time you access them. | ||
Properties with a single type are common enough that we support a "type" for a property, specifiable after its name. | ||
|
||
```ruby | ||
file '/x.txt' do | ||
content lazy { IO.read("/otherfile.txt") } | ||
class MyResource < Chef::Resource | ||
property :content, String | ||
end | ||
``` | ||
|
||
### Validation | ||
This is actually an alias for `is` (described later here). | ||
|
||
There are a number of validation parameters to `property` that affect its behavior. If multiple of these are specified, they must *all* succeed. | ||
|
||
#### callbacks, kind_of, respond_to, cannot_be, regex, equal_to, required | ||
|
||
These function identically to the equivalent functionality on `attribute`. | ||
#### Reusing types | ||
|
||
##### RSpec matchers | ||
The type of a property is represented by `Chef::Resource::PropertyType`, and contains accessors for all the properties of a type (`must_be`, `name_attribute`, `kind_of`, etc.). | ||
|
||
`must` allows for rspec 3 matchers to be passed, and will validate them and print failures. | ||
When you declare `property :name, <type>, <options>`, one of two things happens: | ||
|
||
```ruby | ||
include RSpec::Matchers | ||
property :path, String, must: start_with('/') | ||
``` | ||
- If the type is a PropertyType instance, it is dup'd and any <option>s are set on the new type. | ||
- If the type is a Class, a new PropertyType instance is created with `kind_of` set to [Class] and any options are set on the new type. | ||
- If type is not passed, a new PropertyType instance is created, and any options are set on the new type. | ||
|
||
#### must_match | ||
#### Property Inheritance | ||
|
||
This new option takes an array of values which use Ruby's universal matching | ||
operator, `===`. This means that you can type this: | ||
Subclasses get properties from their parent. | ||
|
||
```ruby | ||
property :x, must_match: [String, :a, :b, nil] | ||
class A < Chef::Resource | ||
property :a, String | ||
end | ||
class B < Chef::Resource | ||
property :b, String | ||
end | ||
B.state_attrs -> [ :a, :b ] | ||
``` | ||
|
||
### Other options | ||
|
||
#### coerce | ||
#### Overriding Properties | ||
|
||
`coerce` is a proc run when the user sets a non-lazy value, or reads a lazy or default value. It allows normalization of input, which makes it simple to create expressive interfaces while preserving a simple programming model that knows what to expect: | ||
When a property is overridden, the override is *complete*: that is, the parent | ||
type is not extended or mixed in any way. | ||
|
||
```ruby | ||
class File < Chef::Resource | ||
attribute :mode, coerce: proc { |v| mode.is_a?(String) ? mode.to_s(8) : mode } | ||
class A < Chef::Resource | ||
property :a, String, default: 'Hello' | ||
end | ||
class B < A | ||
property :a | ||
end | ||
A.properties[:a].default #=> 'Hello' | ||
B.properties[:a].default #=> nil | ||
``` | ||
|
||
`coerce` procs are run in the context of the instance, so that they have access to other attributes and methods. | ||
|
||
#### default | ||
#### `is` | ||
|
||
This works similarly to `attribute`, except that `lazy` values are automatically run in the context of the *instance*: | ||
`is` is a new validation parameter that uses Ruby's match operator `===` (the thing that drives `case` and `when`). | ||
|
||
```ruby | ||
class MysqlInstall < Chef::Resource | ||
property :root_path, String, default: '/' | ||
property :config_path, String, default: lazy { File.join(root_path, 'config') } | ||
end | ||
# These are equivalent | ||
property :x, [ :a, :b, :c ] | ||
property :x, is: [ :a, :b, :c ] | ||
``` | ||
|
||
#### name_attribute | ||
|
||
Same as before. Causes the attribute to be explicitly set to the name passed to the constructor. To wit: | ||
It is worth noting that many existing validations can be expressed directly in terms of `is`: | ||
|
||
```ruby | ||
class MyResource < Chef::Resource | ||
property :path, name_attribute: true | ||
end | ||
|
||
my_resource 'foo' do | ||
name 'bar' | ||
puts path #=> foo | ||
end | ||
# These: | ||
attribute :path, kind_of: String | ||
attribute :path, equal_to: [ :a, :b, :c ] | ||
attribute :path, regex: /^\// | ||
attribute :path, respond_to: :merge | ||
attribute :path, cannot_be: :empty | ||
property :path, is: String | ||
property :path, is: [ :a, :b, :c ] | ||
property :path, is: /^\// | ||
property :path, is: proc { |v| v.respond_to?(:merge) } | ||
property :path, is: proc { |v| !v.empty? } | ||
``` | ||
|
||
#### patchy | ||
|
||
Properties declare whether they are patchy (meaning resource actions will not change the on-disk value) or not patchy by specifying `patchy: true|false`. The primary effect of this is to prevent the property from being cloned during Chef's clone process (which happens when you declare a resource twice with different properties): | ||
|
||
As well as some things that were hard to express before: | ||
|
||
```ruby | ||
file '/mystuff/x.txt' do | ||
mode 0666 | ||
end | ||
property :path, [ String, :up, :down, nil ] | ||
``` | ||
|
||
<long series of actions ...> | ||
If both `is` and a type are specified, the values in the type are prepended to `is`. | ||
|
||
execute 'chmod /mystuff 0777' | ||
### RSpec matchers | ||
|
||
<long series of actions ...> | ||
`is` allows for rspec 3 matchers to be passed, and will validate them and print failures. | ||
|
||
# If mode is declared `patchy: false`, we will change mode back to 0666 here. | ||
# If mode is declared `patchy: true`, we leave mode at 0777. | ||
file '/mystuff/x.txt' do | ||
content 'Hello World' | ||
end | ||
```ruby | ||
include RSpec::Matchers | ||
property :path, a_string_starting_with('/') | ||
``` | ||
|
||
The reason being, a patchy property is one that *leaves the value alone* unless the user actually says they want to change it. Cloning values from other resources violates that. | ||
|
||
### Property type | ||
### Coercion | ||
|
||
Properties with a single type are common enough that we support a primary "type" for a property, specifiable after its name. | ||
`coerce` is a proc run when the user sets a non-lazy value, or reads a lazy or default value. It allows normalization of input, which makes it simple to create expressive interfaces while preserving a simple programming model that knows what to expect: | ||
|
||
```ruby | ||
class MyResource < Chef::Resource | ||
property :content, String | ||
class File < Chef::Resource | ||
attribute :mode, coerce: proc { |m| m.is_a?(String) ? m.to_s(8) : m } | ||
end | ||
``` | ||
|
||
#### Reusing types | ||
|
||
The type of a property is represented by `Chef::Resource::PropertyType`, and contains accessors for all the properties of a type (`must_be`, `name_attribute`, `kind_of`, etc.). | ||
|
||
When you declare `property :name, <type>, <options>`, one of two things happens: | ||
|
||
- If the type is a PropertyType instance, it is dup'd and any <option>s are set on the new type. | ||
- If the type is a Class, a new PropertyType instance is created with `kind_of` set to [Class] and any options are set on the new type. | ||
- If type is not passed, a new PropertyType instance is created, and any options are set on the new type. | ||
`coerce` procs are run in the context of the instance, so that they have access to other attributes and methods. | ||
|
||
### Property Inheritance | ||
### `property_is_set?` | ||
|
||
Subclasses get properties from their parent. | ||
We introduce `property_is_set?(:blah)` to determine whether a given property has been explicitly set on an instance (so you can distinguish between default and non-default values). | ||
|
||
```ruby | ||
class A < Chef::Resource | ||
property :a, String | ||
class X < Chef::Resource | ||
provides :x | ||
property :a, default: 1 | ||
property :b, default: 1 | ||
end | ||
class B < Chef::Resource | ||
property :b, String | ||
|
||
x 'blah' do | ||
a 1 | ||
puts a #=> 1 | ||
puts property_is_set?(:a) #=> true | ||
puts b #=> 1 | ||
puts property_is_set?(:b) #=> false | ||
end | ||
B.state_attrs -> [ :a, :b ] | ||
``` | ||
|
||
#### Overriding Properties | ||
## Backwards Compatibility Summary | ||
|
||
When a property is overridden, the override is *complete*: that is, the parent | ||
type is not extended or mixed in any way. | ||
Two things will change in Chef 13: | ||
|
||
```ruby | ||
class A < Chef::Resource | ||
property :a, String, default: 'Hello' | ||
end | ||
class B < A | ||
property :a | ||
end | ||
A.properties[:a].default #=> 'Hello' | ||
B.properties[:a].default #=> nil | ||
``` | ||
- Lazy defaults: `attribute :x, default: lazy { name }` will run in the context of the instance. | ||
- `nil` is a valid value: `path nil` will set the value of path to nil (or throw a validation error if it is not a valid value). | ||
|
||
## Copyright | ||
|
||
|
Isn't the thing we are deprecating the reverse, not doing the set?