Skip to content

Commit

Permalink
Retool based on comments
Browse files Browse the repository at this point in the history
  • Loading branch information
jkeiser committed May 27, 2015
1 parent 480034d commit 326ec8d
Showing 1 changed file with 122 additions and 105 deletions.
227 changes: 122 additions & 105 deletions new/property.md
Expand Up @@ -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.

Copy link
@coderanger

coderanger May 27, 2015

Contributor

Isn't the thing we are deprecating the reverse, not doing the set?

This comment has been minimized.

Copy link
@jkeiser

jkeiser May 28, 2015

Author Contributor

Yes :)


### 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.

Copy link
@coderanger

coderanger May 27, 2015

Contributor

Which move this to the top since you use it in examples above this section.


`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
Expand All @@ -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

Expand Down

0 comments on commit 326ec8d

Please sign in to comment.