stannum 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +85 -21
- data/lib/stannum/constraints/hashes/extra_keys.rb +7 -2
- data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
- data/lib/stannum/constraints/hashes.rb +6 -2
- data/lib/stannum/constraints/properties/base.rb +124 -0
- data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
- data/lib/stannum/constraints/properties/match_property.rb +117 -0
- data/lib/stannum/constraints/properties/matching.rb +112 -0
- data/lib/stannum/constraints/properties.rb +17 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +1 -1
- data/lib/stannum/constraints/type.rb +1 -1
- data/lib/stannum/constraints.rb +1 -0
- data/lib/stannum/contracts/builder.rb +13 -2
- data/lib/stannum/contracts/indifferent_hash_contract.rb +13 -0
- data/lib/stannum/contracts/tuple_contract.rb +1 -1
- data/lib/stannum/entities/attributes.rb +218 -0
- data/lib/stannum/entities/constraints.rb +177 -0
- data/lib/stannum/entities/properties.rb +186 -0
- data/lib/stannum/entities.rb +13 -0
- data/lib/stannum/entity.rb +83 -0
- data/lib/stannum/errors.rb +3 -3
- data/lib/stannum/rspec/match_errors_matcher.rb +6 -6
- data/lib/stannum/rspec/validate_parameter_matcher.rb +7 -7
- data/lib/stannum/schema.rb +78 -37
- data/lib/stannum/struct.rb +12 -346
- data/lib/stannum/version.rb +1 -1
- data/lib/stannum.rb +3 -0
- metadata +25 -19
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/properties'
|
4
|
+
require 'stannum/constraints/properties/matching'
|
5
|
+
|
6
|
+
module Stannum::Constraints::Properties
|
7
|
+
# Compares the properties of the given object with the specified property.
|
8
|
+
#
|
9
|
+
# If all of the property values equal the expected value, the constraint will
|
10
|
+
# match the object; otherwise, if there are any non-matching values, the
|
11
|
+
# constraint will not match.
|
12
|
+
#
|
13
|
+
# @example Using an Properties::Match constraint
|
14
|
+
# ConfirmPassword = Struct.new(:password, :confirmation)
|
15
|
+
# constraint = Stannum::Constraints::Properties::MatchProperty.new(
|
16
|
+
# :password,
|
17
|
+
# :confirmation
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# params = ConfirmPassword.new('tronlives', 'ifightfortheusers')
|
21
|
+
# constraint.matches?(params)
|
22
|
+
# #=> false
|
23
|
+
# constraint.errors_for(params)
|
24
|
+
# #=> [
|
25
|
+
# {
|
26
|
+
# path: [:confirmation],
|
27
|
+
# type: 'stannum.constraints.is_not_equal_to',
|
28
|
+
# data: { expected: '[FILTERED]', actual: '[FILTERED]' }
|
29
|
+
# }
|
30
|
+
# ]
|
31
|
+
#
|
32
|
+
# params = ConfirmPassword.new('tronlives', 'tronlives')
|
33
|
+
# constraint.matches?(params)
|
34
|
+
# #=> true
|
35
|
+
class MatchProperty < Stannum::Constraints::Properties::Matching
|
36
|
+
# The :type of the error generated for a matching object.
|
37
|
+
NEGATED_TYPE = Stannum::Constraints::Equality::NEGATED_TYPE
|
38
|
+
|
39
|
+
# The :type of the error generated for a non-matching object.
|
40
|
+
TYPE = Stannum::Constraints::Equality::TYPE
|
41
|
+
|
42
|
+
# @return [true, false] false if any of the property values match the
|
43
|
+
# reference property value; otherwise true.
|
44
|
+
def does_not_match?(actual)
|
45
|
+
return false unless can_match_properties?(actual)
|
46
|
+
|
47
|
+
expected = expected_value(actual)
|
48
|
+
|
49
|
+
return false if skip_property?(expected)
|
50
|
+
|
51
|
+
each_matching_property(
|
52
|
+
actual: actual,
|
53
|
+
expected: expected,
|
54
|
+
include_all: true
|
55
|
+
)
|
56
|
+
.none?
|
57
|
+
end
|
58
|
+
|
59
|
+
# (see Stannum::Constraints::Base#errors_for)
|
60
|
+
def errors_for(actual, errors: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
61
|
+
errors ||= Stannum::Errors.new
|
62
|
+
|
63
|
+
return invalid_object_errors(errors) unless can_match_properties?(actual)
|
64
|
+
|
65
|
+
expected = expected_value(actual)
|
66
|
+
matching = each_non_matching_property(actual: actual, expected: expected)
|
67
|
+
|
68
|
+
return generic_errors(errors) if matching.count.zero?
|
69
|
+
|
70
|
+
matching.each do |property_name, value|
|
71
|
+
errors[property_name].add(
|
72
|
+
type,
|
73
|
+
message: message,
|
74
|
+
expected: filter_parameters? ? '[FILTERED]' : expected_value(actual),
|
75
|
+
actual: filter_parameters? ? '[FILTERED]' : value
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
errors
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [true, false] true if the property values match the reference
|
83
|
+
# property value; otherwise false.
|
84
|
+
def matches?(actual)
|
85
|
+
return false unless can_match_properties?(actual)
|
86
|
+
|
87
|
+
expected = expected_value(actual)
|
88
|
+
|
89
|
+
return true if skip_property?(expected)
|
90
|
+
|
91
|
+
each_non_matching_property(actual: actual, expected: expected).none?
|
92
|
+
end
|
93
|
+
alias match? matches?
|
94
|
+
|
95
|
+
# (see Stannum::Constraints::Base#negated_errors_for)
|
96
|
+
def negated_errors_for(actual, errors: nil) # rubocop:disable Metrics/MethodLength
|
97
|
+
errors ||= Stannum::Errors.new
|
98
|
+
|
99
|
+
return invalid_object_errors(errors) unless can_match_properties?(actual)
|
100
|
+
|
101
|
+
expected = expected_value(actual)
|
102
|
+
matching = each_matching_property(
|
103
|
+
actual: actual,
|
104
|
+
expected: expected,
|
105
|
+
include_all: true
|
106
|
+
)
|
107
|
+
|
108
|
+
return generic_errors(errors) if matching.count.zero?
|
109
|
+
|
110
|
+
matching.each do |property_name, _|
|
111
|
+
errors[property_name].add(negated_type, message: negated_message)
|
112
|
+
end
|
113
|
+
|
114
|
+
errors
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/properties'
|
4
|
+
require 'stannum/constraints/properties/base'
|
5
|
+
|
6
|
+
module Stannum::Constraints::Properties
|
7
|
+
# Abstract base class for property matching constraints.
|
8
|
+
class Matching < Stannum::Constraints::Properties::Base
|
9
|
+
# @param reference_name [String, Symbol] the name of the reference property
|
10
|
+
# to compare to.
|
11
|
+
# @param property_names [Array<String, Symbol>] the name or names of the
|
12
|
+
# properties to compare.
|
13
|
+
# @param options [Hash<Symbol, Object>] configuration options for the
|
14
|
+
# constraint. Defaults to an empty Hash.
|
15
|
+
#
|
16
|
+
# @option options allow_empty [true, false] if true, will match against an
|
17
|
+
# object with empty property values, such as an empty string.
|
18
|
+
# @option options allow_nil [true, false] if true, will match against an
|
19
|
+
# object with nil property values.
|
20
|
+
def initialize(reference_name, *property_names, **options)
|
21
|
+
@reference_name = reference_name
|
22
|
+
|
23
|
+
validate_reference_name
|
24
|
+
|
25
|
+
super(*property_names, reference_name: reference_name, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String, Symbol] the name of the reference property to compare to.
|
29
|
+
attr_reader :reference_name
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def each_matching_property( # rubocop:disable Metrics/MethodLength
|
34
|
+
actual:,
|
35
|
+
expected:,
|
36
|
+
include_all: false,
|
37
|
+
&block
|
38
|
+
)
|
39
|
+
unless block_given?
|
40
|
+
return to_enum(
|
41
|
+
__method__,
|
42
|
+
actual: actual,
|
43
|
+
expected: expected,
|
44
|
+
include_all: include_all
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
enumerator = each_property(actual)
|
49
|
+
|
50
|
+
unless include_all
|
51
|
+
enumerator = enumerator.reject { |_, value| skip_property?(value) }
|
52
|
+
end
|
53
|
+
|
54
|
+
enumerator = enumerator.select { |_, value| valid?(expected, value) }
|
55
|
+
|
56
|
+
block_given? ? enumerator.each(&block) : enumerator
|
57
|
+
end
|
58
|
+
|
59
|
+
def each_non_matching_property( # rubocop:disable Metrics/MethodLength
|
60
|
+
actual:,
|
61
|
+
expected:,
|
62
|
+
include_all: false,
|
63
|
+
&block
|
64
|
+
)
|
65
|
+
unless block_given?
|
66
|
+
return to_enum(
|
67
|
+
__method__,
|
68
|
+
actual: actual,
|
69
|
+
expected: expected,
|
70
|
+
include_all: include_all
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
enumerator = each_property(actual)
|
75
|
+
|
76
|
+
unless include_all
|
77
|
+
enumerator = enumerator.reject { |_, value| skip_property?(value) }
|
78
|
+
end
|
79
|
+
|
80
|
+
enumerator = enumerator.reject { |_, value| valid?(expected, value) }
|
81
|
+
|
82
|
+
block_given? ? enumerator.each(&block) : enumerator
|
83
|
+
end
|
84
|
+
|
85
|
+
def expected_value(actual)
|
86
|
+
actual[reference_name]
|
87
|
+
end
|
88
|
+
|
89
|
+
def filter_parameters?
|
90
|
+
return @filter_parameters unless @filter_parameters.nil?
|
91
|
+
|
92
|
+
filters = filtered_parameters.map { |param| Regexp.new(param.to_s) }
|
93
|
+
|
94
|
+
@filter_parameters =
|
95
|
+
[reference_name, *property_names].any? do |property_name|
|
96
|
+
filters.any? { |filter| filter.match?(property_name.to_s) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def generic_errors(errors)
|
101
|
+
errors.add(Stannum::Constraints::Base::NEGATED_TYPE)
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid?(expected, value)
|
105
|
+
value == expected
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_reference_name
|
109
|
+
tools.assertions.validate_name(reference_name, as: 'reference name')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# Namespace for Object property-specific constraints.
|
7
|
+
module Properties
|
8
|
+
autoload :Base,
|
9
|
+
'stannum/constraints/properties/base'
|
10
|
+
autoload :DoNotMatchProperty,
|
11
|
+
'stannum/constraints/properties/do_not_match_property'
|
12
|
+
autoload :MatchProperty,
|
13
|
+
'stannum/constraints/properties/match_property'
|
14
|
+
autoload :Matching,
|
15
|
+
'stannum/constraints/properties/matching'
|
16
|
+
end
|
17
|
+
end
|
@@ -78,7 +78,7 @@ module Stannum::Constraints::Tuples
|
|
78
78
|
def each_extra_item(actual, &block)
|
79
79
|
return if matches?(actual)
|
80
80
|
|
81
|
-
actual[expected_count
|
81
|
+
actual[expected_count..].each.with_index(expected_count, &block)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
end
|
data/lib/stannum/constraints.rb
CHANGED
@@ -17,6 +17,7 @@ module Stannum
|
|
17
17
|
autoload :Nothing, 'stannum/constraints/nothing'
|
18
18
|
autoload :Parameters, 'stannum/constraints/parameters'
|
19
19
|
autoload :Presence, 'stannum/constraints/presence'
|
20
|
+
autoload :Properties, 'stannum/constraints/properties'
|
20
21
|
autoload :Signature, 'stannum/constraints/signature'
|
21
22
|
autoload :Signatures, 'stannum/constraints/signatures'
|
22
23
|
autoload :Tuples, 'stannum/constraints/tuples'
|
@@ -18,6 +18,17 @@ module Stannum::Contracts
|
|
18
18
|
# @return [Stannum::Contract] The contract to which constraints are added.
|
19
19
|
attr_reader :contract
|
20
20
|
|
21
|
+
# Concatenate the constraints from the given other contract.
|
22
|
+
#
|
23
|
+
# @param other [Stannum::Contract] the other contract.
|
24
|
+
#
|
25
|
+
# @return [self] the contract builder.
|
26
|
+
#
|
27
|
+
# @see Stannum::Contracts::Base#concat.
|
28
|
+
def concat(other)
|
29
|
+
contract.concat(other)
|
30
|
+
end
|
31
|
+
|
21
32
|
# Adds a constraint to the contract.
|
22
33
|
#
|
23
34
|
# @overload constraint(constraint, **options)
|
@@ -47,8 +58,8 @@ module Stannum::Contracts
|
|
47
58
|
private
|
48
59
|
|
49
60
|
def ambiguous_values_error(constraint)
|
50
|
-
'expected either a block or a constraint instance, but received both a' \
|
51
|
-
"
|
61
|
+
'expected either a block or a constraint instance, but received both a ' \
|
62
|
+
"block and #{constraint.inspect}"
|
52
63
|
end
|
53
64
|
|
54
65
|
def resolve_constraint(constraint = nil, **options, &block)
|
@@ -74,5 +74,18 @@ module Stannum::Contracts
|
|
74
74
|
actual.fetch(property) { actual[property.to_s] }
|
75
75
|
end
|
76
76
|
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def add_extra_keys_constraint
|
81
|
+
return if options[:allow_extra_keys]
|
82
|
+
|
83
|
+
keys = -> { expected_keys }
|
84
|
+
|
85
|
+
add_constraint(
|
86
|
+
Stannum::Constraints::Hashes::IndifferentExtraKeys.new(keys),
|
87
|
+
concatenatable: false
|
88
|
+
)
|
89
|
+
end
|
77
90
|
end
|
78
91
|
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/entities'
|
4
|
+
require 'stannum/schema'
|
5
|
+
|
6
|
+
module Stannum::Entities
|
7
|
+
# Methods for defining and accessing entity attributes.
|
8
|
+
module Attributes
|
9
|
+
# Class methods to extend the class when including Attributes.
|
10
|
+
module ClassMethods
|
11
|
+
# Defines an attribute on the entity.
|
12
|
+
#
|
13
|
+
# When an attribute is defined, each of the following steps is executed:
|
14
|
+
#
|
15
|
+
# - Adds the attribute to ::Attributes and the .attributes class method.
|
16
|
+
# - Adds the attribute to #attributes and the associated methods, such as
|
17
|
+
# #assign_attributes, #[] and #[]=.
|
18
|
+
# - Defines reader and writer methods.
|
19
|
+
#
|
20
|
+
# @param attr_name [String, Symbol] The name of the attribute. Must be a
|
21
|
+
# non-empty String or Symbol.
|
22
|
+
# @param attr_type [Class, String] The type of the attribute. Must be a
|
23
|
+
# Class or Module, or the name of a class or module.
|
24
|
+
# @param options [Hash] Additional options for the attribute.
|
25
|
+
#
|
26
|
+
# @option options [Object] :default The default value for the attribute.
|
27
|
+
# Defaults to nil.
|
28
|
+
#
|
29
|
+
# @return [Symbol] The attribute name as a symbol.
|
30
|
+
def attribute(attr_name, attr_type, **options)
|
31
|
+
attributes.define_attribute(
|
32
|
+
name: attr_name,
|
33
|
+
type: attr_type,
|
34
|
+
options: options
|
35
|
+
)
|
36
|
+
|
37
|
+
attr_name.intern
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Stannum::Schema] The attributes Schema object for the Entity.
|
41
|
+
def attributes
|
42
|
+
self::Attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def included(other)
|
48
|
+
super
|
49
|
+
|
50
|
+
other.include(Stannum::Entities::Attributes)
|
51
|
+
|
52
|
+
Stannum::Entities::Attributes.apply(other) if other.is_a?(Class)
|
53
|
+
end
|
54
|
+
|
55
|
+
def inherited(other)
|
56
|
+
super
|
57
|
+
|
58
|
+
Stannum::Entities::Attributes.apply(other)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class << self
|
63
|
+
# Generates Attributes schema for the class.
|
64
|
+
#
|
65
|
+
# Creates a new Stannum::Schema and sets it as the class's :Attributes
|
66
|
+
# constant. If the superclass is an entity class (and already defines its
|
67
|
+
# own Attributes, includes the superclass Attributes in the class
|
68
|
+
# Attributes). Finally, includes the class Attributes in the class.
|
69
|
+
#
|
70
|
+
# @param other [Class] the class to which attributes are added.
|
71
|
+
def apply(other)
|
72
|
+
return unless other.is_a?(Class)
|
73
|
+
|
74
|
+
return if entity_class?(other)
|
75
|
+
|
76
|
+
other.const_set(:Attributes, Stannum::Schema.new)
|
77
|
+
|
78
|
+
if entity_class?(other.superclass)
|
79
|
+
other::Attributes.include(other.superclass::Attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
other.include(other::Attributes)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def entity_class?(other)
|
88
|
+
other.const_defined?(:Attributes, false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def included(other)
|
92
|
+
super
|
93
|
+
|
94
|
+
other.extend(self::ClassMethods)
|
95
|
+
|
96
|
+
apply(other) if other.is_a?(Class)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param properties [Hash] the properties used to initialize the entity.
|
101
|
+
def initialize(**properties)
|
102
|
+
@attributes = {}
|
103
|
+
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
# Updates the struct's attributes with the given values.
|
108
|
+
#
|
109
|
+
# This method is used to update some (but not all) of the attributes of the
|
110
|
+
# struct. For each key in the hash, it calls the corresponding writer method
|
111
|
+
# with the value for that attribute. If the value is nil, this will set the
|
112
|
+
# attribute value to the default for that attribute.
|
113
|
+
#
|
114
|
+
# Any attributes that are not in the given hash are unchanged, as are any
|
115
|
+
# properties that are not attributes.
|
116
|
+
#
|
117
|
+
# If the attributes hash includes any keys that do not correspond to an
|
118
|
+
# attribute, the struct will raise an error.
|
119
|
+
#
|
120
|
+
# @param attributes [Hash] The initial attributes for the struct.
|
121
|
+
#
|
122
|
+
# @raise ArgumentError if the key is not a valid attribute.
|
123
|
+
#
|
124
|
+
# @see #attributes=
|
125
|
+
def assign_attributes(attributes)
|
126
|
+
unless attributes.is_a?(Hash)
|
127
|
+
raise ArgumentError, 'attributes must be a Hash'
|
128
|
+
end
|
129
|
+
|
130
|
+
set_attributes(attributes, force: false)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Collects the entity attributes.
|
134
|
+
#
|
135
|
+
# @param attributes [Hash<String, Object>] the entity attributes.
|
136
|
+
def attributes
|
137
|
+
@attributes.dup
|
138
|
+
end
|
139
|
+
|
140
|
+
# Replaces the entity's attributes with the given values.
|
141
|
+
#
|
142
|
+
# This method is used to update all of the attributes of the entity. For
|
143
|
+
# each attribute, the writer method is called with the value from the hash,
|
144
|
+
# or nil if the corresponding key is not present in the hash. Any nil or
|
145
|
+
# missing values set the attribute value to that attribute's default value,
|
146
|
+
# if any. Non-attribute properties are unchanged.
|
147
|
+
#
|
148
|
+
# If the attributes hash includes any keys that do not correspond to a valid
|
149
|
+
# attribute, the entity will raise an error.
|
150
|
+
#
|
151
|
+
# @param attributes [Hash] the attributes to assign to the entity.
|
152
|
+
#
|
153
|
+
# @raise ArgumentError if any key is not a valid attribute.
|
154
|
+
#
|
155
|
+
# @see #assign_attributes
|
156
|
+
def attributes=(attributes)
|
157
|
+
unless attributes.is_a?(Hash)
|
158
|
+
raise ArgumentError, 'attributes must be a Hash'
|
159
|
+
end
|
160
|
+
|
161
|
+
set_attributes(attributes, force: true)
|
162
|
+
end
|
163
|
+
|
164
|
+
# (see Stannum::Entities::Properties#properties)
|
165
|
+
def properties
|
166
|
+
super.merge(attributes)
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def get_property(key)
|
172
|
+
return @attributes[key.to_s] if attributes.key?(key.to_s)
|
173
|
+
|
174
|
+
super
|
175
|
+
end
|
176
|
+
|
177
|
+
def inspectable_properties
|
178
|
+
super().merge(attributes)
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_attributes(attributes, force:)
|
182
|
+
attributes, non_matching =
|
183
|
+
bisect_properties(attributes, self.class.attributes)
|
184
|
+
|
185
|
+
unless non_matching.empty?
|
186
|
+
handle_invalid_properties(non_matching, as: 'attribute')
|
187
|
+
end
|
188
|
+
|
189
|
+
write_attributes(attributes, force: force)
|
190
|
+
end
|
191
|
+
|
192
|
+
def set_properties(properties, force:)
|
193
|
+
attributes, non_matching =
|
194
|
+
bisect_properties(properties, self.class.attributes)
|
195
|
+
|
196
|
+
super(non_matching, force: force)
|
197
|
+
|
198
|
+
write_attributes(attributes, force: force)
|
199
|
+
end
|
200
|
+
|
201
|
+
def set_property(key, value)
|
202
|
+
return super unless attributes.key?(key.to_s)
|
203
|
+
|
204
|
+
send(self.class.attributes[key.to_s].writer_name, value)
|
205
|
+
end
|
206
|
+
|
207
|
+
def write_attributes(attributes, force:)
|
208
|
+
self.class.attributes.each do |attr_name, attribute|
|
209
|
+
next unless attributes.key?(attr_name) || force
|
210
|
+
|
211
|
+
send(
|
212
|
+
attribute.writer_name,
|
213
|
+
attributes[attr_name].nil? ? attribute.default : attributes[attr_name]
|
214
|
+
)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|