cattri 0.1.3 → 0.2.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/.github/workflows/main.yml +34 -0
- data/.gitignore +72 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +41 -0
- data/Gemfile +12 -0
- data/README.md +163 -151
- data/Steepfile +6 -0
- data/bin/console +33 -0
- data/bin/setup +8 -0
- data/cattri.gemspec +5 -5
- data/lib/cattri/attribute.rb +119 -155
- data/lib/cattri/attribute_compiler.rb +104 -0
- data/lib/cattri/attribute_options.rb +183 -0
- data/lib/cattri/attribute_registry.rb +155 -0
- data/lib/cattri/context.rb +124 -106
- data/lib/cattri/context_registry.rb +36 -0
- data/lib/cattri/deferred_attributes.rb +73 -0
- data/lib/cattri/dsl.rb +54 -0
- data/lib/cattri/error.rb +17 -90
- data/lib/cattri/inheritance.rb +35 -0
- data/lib/cattri/initializer_patch.rb +37 -0
- data/lib/cattri/internal_store.rb +104 -0
- data/lib/cattri/introspection.rb +56 -49
- data/lib/cattri/version.rb +3 -1
- data/lib/cattri.rb +38 -99
- data/sig/lib/cattri/attribute.rbs +105 -0
- data/sig/lib/cattri/attribute_compiler.rbs +61 -0
- data/sig/lib/cattri/attribute_options.rbs +150 -0
- data/sig/lib/cattri/attribute_registry.rbs +95 -0
- data/sig/lib/cattri/context.rbs +130 -0
- data/sig/lib/cattri/context_registry.rbs +31 -0
- data/sig/lib/cattri/deferred_attributes.rbs +53 -0
- data/sig/lib/cattri/dsl.rbs +55 -0
- data/sig/lib/cattri/error.rbs +28 -0
- data/sig/lib/cattri/inheritance.rbs +21 -0
- data/sig/lib/cattri/initializer_patch.rbs +26 -0
- data/sig/lib/cattri/internal_store.rbs +75 -0
- data/sig/lib/cattri/introspection.rbs +61 -0
- data/sig/lib/cattri/types.rbs +19 -0
- data/sig/lib/cattri/visibility.rbs +55 -0
- data/sig/lib/cattri.rbs +37 -0
- data/spec/cattri/attribute_compiler_spec.rb +179 -0
- data/spec/cattri/attribute_options_spec.rb +267 -0
- data/spec/cattri/attribute_registry_spec.rb +257 -0
- data/spec/cattri/attribute_spec.rb +297 -0
- data/spec/cattri/context_registry_spec.rb +45 -0
- data/spec/cattri/context_spec.rb +346 -0
- data/spec/cattri/deferred_attrributes_spec.rb +117 -0
- data/spec/cattri/dsl_spec.rb +69 -0
- data/spec/cattri/error_spec.rb +37 -0
- data/spec/cattri/inheritance_spec.rb +60 -0
- data/spec/cattri/initializer_patch_spec.rb +35 -0
- data/spec/cattri/internal_store_spec.rb +139 -0
- data/spec/cattri/introspection_spec.rb +90 -0
- data/spec/cattri/visibility_spec.rb +68 -0
- data/spec/cattri_spec.rb +54 -0
- data/spec/simplecov_helper.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +79 -6
- data/lib/cattri/attribute_definer.rb +0 -143
- data/lib/cattri/class_attributes.rb +0 -277
- data/lib/cattri/instance_attributes.rb +0 -276
- data/sig/cattri.rbs +0 -4
@@ -0,0 +1,28 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Base error class for all exceptions raised by Cattri.
|
3
|
+
#
|
4
|
+
# All Cattri-specific errors inherit from this class, allowing unified
|
5
|
+
# rescue handling at the framework level.
|
6
|
+
#
|
7
|
+
# The backtrace is preserved and may be filtered before raising.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# rescue Cattri::Error => e
|
11
|
+
# puts "Something went wrong with Cattri: #{e.message}"
|
12
|
+
class Error < StandardError
|
13
|
+
# Initializes the error with an optional message and caller backtrace.
|
14
|
+
#
|
15
|
+
# @param msg [String, nil] the error message
|
16
|
+
# @param backtrace [Array<String>] optional backtrace (defaults to `caller`)
|
17
|
+
def initialize: (?::String? msg, ?::Array[::String] backtrace) -> void
|
18
|
+
end
|
19
|
+
|
20
|
+
# Raised for any attribute-related definition or usage failure.
|
21
|
+
#
|
22
|
+
# This includes method conflicts, invalid configuration, and
|
23
|
+
# write attempts on `final` attributes.
|
24
|
+
#
|
25
|
+
# @see Cattri::Error
|
26
|
+
class AttributeError < Error
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Handles subclassing behavior for classes that use Cattri.
|
3
|
+
#
|
4
|
+
# This module installs a custom `.inherited` hook on the target class's singleton,
|
5
|
+
# ensuring that attribute definitions and values are deep-copied to the subclass.
|
6
|
+
#
|
7
|
+
# The hook preserves any existing `.inherited` behavior defined on the class,
|
8
|
+
# calling it before applying attribute propagation.
|
9
|
+
module Inheritance
|
10
|
+
# Installs an `inherited` hook on the given class.
|
11
|
+
#
|
12
|
+
# When the class is subclassed, Cattri will copy over attribute metadata and values
|
13
|
+
# using the subclass’s context. This ensures subclass safety and definition isolation.
|
14
|
+
#
|
15
|
+
# Any pre-existing `.inherited` method is preserved and invoked first.
|
16
|
+
#
|
17
|
+
# @param base [Class] the class to install the hook on
|
18
|
+
# @return [void]
|
19
|
+
def self.install: (::Module base) -> void
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Provides a patch to `#initialize` that ensures all final attributes
|
3
|
+
# are initialized with their default values if not already set.
|
4
|
+
#
|
5
|
+
# This module is prepended into Cattri-including classes to enforce
|
6
|
+
# write-once semantics for instance-level `final: true` attributes.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class MyClass
|
10
|
+
# include Cattri
|
11
|
+
#
|
12
|
+
# cattri :id, -> { SecureRandom.uuid }, final: true
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# MyClass.new # => will have a UUID assigned to @id unless explicitly set
|
16
|
+
module InitializerPatch
|
17
|
+
# Hooked constructor that initializes final attributes using their defaults
|
18
|
+
# if no value has been set by the user.
|
19
|
+
#
|
20
|
+
# @param args [Array] any positional arguments passed to initialize
|
21
|
+
# @param kwargs [Hash] any keyword arguments passed to initialize
|
22
|
+
# @yield an optional block to pass to `super`
|
23
|
+
# @return [void]
|
24
|
+
def initialize: (*untyped args, **untyped kwargs) { (?) -> untyped } -> void
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Internal representation of a stored attribute value.
|
3
|
+
AttributeValue: untyped
|
4
|
+
|
5
|
+
# Provides an internal storage mechanism for attribute values defined via `cattri`.
|
6
|
+
#
|
7
|
+
# This module is included into any class or module using Cattri and replaces
|
8
|
+
# direct instance variable access with a namespaced store.
|
9
|
+
#
|
10
|
+
# It supports enforcement of `final` semantics and tracks explicit assignments.
|
11
|
+
module InternalStore
|
12
|
+
@__cattri_store: ::Hash[::Symbol, untyped]
|
13
|
+
|
14
|
+
@__cattri_set_variables: ::Set[::Symbol]
|
15
|
+
|
16
|
+
# Checks whether the internal store contains a value for the given key.
|
17
|
+
#
|
18
|
+
# @param key [String, Symbol] the attribute name or instance variable
|
19
|
+
# @return [Boolean] true if a value is present
|
20
|
+
def cattri_variable_defined?: (identifier key) -> bool
|
21
|
+
|
22
|
+
# Fetches the value for a given attribute key from the internal store.
|
23
|
+
#
|
24
|
+
# @param key [String, Symbol] the attribute name or instance variable
|
25
|
+
# @return [Object, nil] the stored value, or nil if not present
|
26
|
+
def cattri_variable_get: (identifier key) -> untyped
|
27
|
+
|
28
|
+
# Sets a value in the internal store for the given attribute key.
|
29
|
+
#
|
30
|
+
# Enforces final semantics if a final value was already set.
|
31
|
+
#
|
32
|
+
# @param key [String, Symbol] the attribute name or instance variable
|
33
|
+
# @param value [Object] the value to store
|
34
|
+
# @param final [Boolean] whether the value should be locked as final
|
35
|
+
# @return [Object] the stored value
|
36
|
+
def cattri_variable_set: (identifier key, untyped value, ?final: bool) -> untyped
|
37
|
+
|
38
|
+
# Evaluates and sets a value for the given key only if it hasn't already been set.
|
39
|
+
#
|
40
|
+
# If a value is already present, it is returned as-is. Otherwise, the provided block
|
41
|
+
# is called to compute the value, which is then stored. If `final: true` is passed,
|
42
|
+
# the value is marked as final and cannot be reassigned.
|
43
|
+
#
|
44
|
+
# @param key [String, Symbol] the attribute name or instance variable
|
45
|
+
# @param final [Boolean] whether to mark the value as final (immutable once set)
|
46
|
+
# @yieldreturn [Object] the value to memoize if not already present
|
47
|
+
# @return [Object] the existing or newly memoized value
|
48
|
+
# @raise [Cattri::AttributeError] if attempting to overwrite a final value
|
49
|
+
def cattri_variable_memoize: (identifier key, ?final: bool) { () -> untyped } -> untyped
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Returns the internal storage hash used for attribute values.
|
54
|
+
#
|
55
|
+
# @return [Hash<Symbol, Cattri::AttributeValue>]
|
56
|
+
def __cattri_store: () -> ::Hash[::Symbol, untyped]
|
57
|
+
|
58
|
+
# Returns the set of attribute keys that have been explicitly assigned.
|
59
|
+
#
|
60
|
+
# @return [Set<Symbol>]
|
61
|
+
def __cattri_set_variables: () -> ::Set[::Symbol]
|
62
|
+
|
63
|
+
# Normalizes the attribute key to a symbol without `@` prefix.
|
64
|
+
#
|
65
|
+
# @param key [String, Symbol]
|
66
|
+
# @return [Symbol]
|
67
|
+
def normalize_ivar: (identifier key) -> ::Symbol
|
68
|
+
|
69
|
+
# Raises if attempting to modify a value that was marked as final.
|
70
|
+
#
|
71
|
+
# @param key [Symbol]
|
72
|
+
# @raise [Cattri::AttributeError] if the key is final and already set
|
73
|
+
def guard_final!: (::Symbol key) -> void
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Provides a read-only interface for inspecting attributes defined via the Cattri DSL.
|
3
|
+
#
|
4
|
+
# When included, adds class-level methods to:
|
5
|
+
# - Check if an attribute is defined
|
6
|
+
# - Retrieve attribute definitions
|
7
|
+
# - List defined attribute methods
|
8
|
+
# - Trace the origin of an attribute
|
9
|
+
module Introspection
|
10
|
+
# @param base [Class, Module] the target that includes `Cattri`
|
11
|
+
# @return [void]
|
12
|
+
def self.included: (::Module base) -> void
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
# Class-level introspection methods exposed via `.attribute_defined?`, `.attribute`, etc.
|
16
|
+
module ClassMethods
|
17
|
+
# Returns true if the given attribute has been defined on this class or any ancestor.
|
18
|
+
#
|
19
|
+
# @param name [Symbol, String] the attribute name
|
20
|
+
# @return [Boolean]
|
21
|
+
def attribute_defined?: (identifier name) -> bool
|
22
|
+
|
23
|
+
# Returns the attribute definition for the given name.
|
24
|
+
#
|
25
|
+
# Includes inherited definitions if available.
|
26
|
+
#
|
27
|
+
# @param name [Symbol, String] the attribute name
|
28
|
+
# @return [Cattri::Attribute, nil]
|
29
|
+
def attribute: (identifier name) -> Attribute?
|
30
|
+
|
31
|
+
# Returns a list of attribute names defined on this class.
|
32
|
+
#
|
33
|
+
# Includes inherited attributes if `with_ancestors` is true.
|
34
|
+
#
|
35
|
+
# @param with_ancestors [Boolean]
|
36
|
+
# @return [Array<Symbol>]
|
37
|
+
def attributes: (?with_ancestors: bool) -> ::Array[::Symbol]
|
38
|
+
|
39
|
+
# Returns a hash of attribute definitions defined on this class.
|
40
|
+
#
|
41
|
+
# Includes inherited attributes if `with_ancestors` is true.
|
42
|
+
#
|
43
|
+
# @param with_ancestors [Boolean]
|
44
|
+
# @return [Hash{Symbol => Cattri::Attribute}]
|
45
|
+
def attribute_definitions: (?with_ancestors: bool) -> ::Hash[::Symbol, Attribute]
|
46
|
+
|
47
|
+
# Returns a hash of all methods defined by Cattri attributes.
|
48
|
+
#
|
49
|
+
# This includes accessors, writers, and predicates where applicable.
|
50
|
+
#
|
51
|
+
# @return [Hash{Symbol => Set<Symbol>}]
|
52
|
+
def attribute_methods: () -> ::Hash[::Symbol, ::Set[::Symbol]]
|
53
|
+
|
54
|
+
# Returns the original class or module where the given attribute was defined.
|
55
|
+
#
|
56
|
+
# @param name [Symbol, String] the attribute name
|
57
|
+
# @return [Module, nil]
|
58
|
+
def attribute_source: (identifier name) -> ::Module?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Cattri
|
2
|
+
type identifier = ::String | ::Symbol
|
3
|
+
|
4
|
+
type scope_types = :class | :instance
|
5
|
+
|
6
|
+
type expose_types = :read_write | :read | :write | :none
|
7
|
+
|
8
|
+
type visibility_types = :public | :protected | :private
|
9
|
+
|
10
|
+
type attribute_options = {
|
11
|
+
ivar?: identifier,
|
12
|
+
final: bool,
|
13
|
+
scope?: scope_types,
|
14
|
+
predicate?: bool,
|
15
|
+
default?: ::Proc | untyped,
|
16
|
+
expose?: expose_types,
|
17
|
+
visibility?: visibility_types
|
18
|
+
}
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Cattri
|
2
|
+
# Cattri::Visibility tracks the current method visibility context (`public`, `protected`, `private`)
|
3
|
+
# when defining methods dynamically. It mimics Ruby's native visibility behavior so that
|
4
|
+
# `cattr` and `iattr` definitions can automatically infer the intended access level
|
5
|
+
# based on the current context in the source file.
|
6
|
+
#
|
7
|
+
# This module is intended to be extended by classes that include or extend Cattri.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class MyClass
|
11
|
+
# include Cattri
|
12
|
+
#
|
13
|
+
# private
|
14
|
+
# cattr :sensitive_data
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # => :sensitive_data will be defined as a private method
|
18
|
+
module Visibility
|
19
|
+
@__cattri_visibility: visibility_types
|
20
|
+
|
21
|
+
# Returns the currently active visibility scope on the class or module.
|
22
|
+
#
|
23
|
+
# Defaults to `:public` unless changed explicitly via `public`, `protected`, or `private`.
|
24
|
+
#
|
25
|
+
# @return [Symbol] :public, :protected, or :private
|
26
|
+
def __cattri_visibility: () -> visibility_types
|
27
|
+
|
28
|
+
# Intercepts calls to `public` to update the visibility tracker.
|
29
|
+
#
|
30
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
31
|
+
# Otherwise, delegates to Ruby’s native `Module#public`.
|
32
|
+
#
|
33
|
+
# @param args [Array<Symbol>] method names to make public, or empty to set context
|
34
|
+
# @return [void]
|
35
|
+
def public: (*untyped args) -> void
|
36
|
+
|
37
|
+
# Intercepts calls to `protected` to update the visibility tracker.
|
38
|
+
#
|
39
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
40
|
+
# Otherwise, delegates to Ruby’s native `Module#protected`.
|
41
|
+
#
|
42
|
+
# @param args [Array<Symbol>] method names to make protected, or empty to set context
|
43
|
+
# @return [void]
|
44
|
+
def protected: (*untyped args) -> void
|
45
|
+
|
46
|
+
# Intercepts calls to `private` to update the visibility tracker.
|
47
|
+
#
|
48
|
+
# If no method names are passed, this sets the current visibility scope for future methods.
|
49
|
+
# Otherwise, delegates to Ruby’s native `Module#private`.
|
50
|
+
#
|
51
|
+
# @param args [Array<Symbol>] method names to make private, or empty to set context
|
52
|
+
# @return [void]
|
53
|
+
def private: (*untyped args) -> void
|
54
|
+
end
|
55
|
+
end
|
data/sig/lib/cattri.rbs
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Main entrypoint for the Cattri DSL.
|
2
|
+
#
|
3
|
+
# When included in a class or module, this installs both class-level and instance-level
|
4
|
+
# attribute handling logic, visibility tracking, subclass inheritance propagation,
|
5
|
+
# and default value enforcement.
|
6
|
+
#
|
7
|
+
# It supports:
|
8
|
+
# - Defining class or instance attributes using `cattri` or `final_cattri`
|
9
|
+
# - Visibility tracking and method scoping
|
10
|
+
# - Write-once semantics for `final: true` attributes
|
11
|
+
# - Safe method generation with introspection support
|
12
|
+
module Cattri
|
13
|
+
VERSION: ::String
|
14
|
+
|
15
|
+
# Sets up the Cattri DSL on the including class or module.
|
16
|
+
#
|
17
|
+
# Includes internal storage and registry infrastructure into both the base and its singleton class,
|
18
|
+
# prepends initialization logic, extends visibility and DSL handling, and installs the
|
19
|
+
# subclassing hook to propagate attributes to descendants.
|
20
|
+
#
|
21
|
+
# @param base [Class, Module] the target that includes `Cattri`
|
22
|
+
# @return [void]
|
23
|
+
def self.included: (::Module base) -> void
|
24
|
+
|
25
|
+
# Provides opt-in class-level introspection support.
|
26
|
+
#
|
27
|
+
# This allows users to call methods like `.attribute_defined?`, `.attribute_methods`, etc.,
|
28
|
+
# to inspect which attributes have been defined.
|
29
|
+
module ClassMethods
|
30
|
+
# Enables Cattri's attribute introspection methods on the current class.
|
31
|
+
#
|
32
|
+
# @return [void]
|
33
|
+
def with_cattri_introspection: () -> void
|
34
|
+
|
35
|
+
include Introspection
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Cattri::AttributeCompiler do
|
6
|
+
let(:dummy_class) do
|
7
|
+
Class.new do
|
8
|
+
include Cattri
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:context) { dummy_class.send(:context) }
|
13
|
+
|
14
|
+
describe ".define_accessor" do
|
15
|
+
context "when attribute is final and class-level" do
|
16
|
+
let(:attribute) do
|
17
|
+
Cattri::Attribute.new(
|
18
|
+
:count,
|
19
|
+
defined_in: dummy_class,
|
20
|
+
scope: :class,
|
21
|
+
final: true,
|
22
|
+
default: -> { 100 },
|
23
|
+
expose: :read_write
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "eagerly sets the default value on the target class" do
|
28
|
+
described_class.define_accessor(attribute, context)
|
29
|
+
expect(dummy_class.cattri_variable_get(:count)).to eq(100)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when expose is :none" do
|
34
|
+
let(:attribute) do
|
35
|
+
Cattri::Attribute.new(
|
36
|
+
:secret,
|
37
|
+
defined_in: dummy_class,
|
38
|
+
default: -> { "hidden" },
|
39
|
+
expose: :none
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "does not define any methods" do
|
44
|
+
described_class.define_accessor(attribute, context)
|
45
|
+
instance = dummy_class.new
|
46
|
+
expect(instance).not_to respond_to(:secret)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when expose is :read_write with predicate" do
|
51
|
+
let(:attribute) do
|
52
|
+
Cattri::Attribute.new(
|
53
|
+
:enabled,
|
54
|
+
defined_in: dummy_class,
|
55
|
+
default: -> { false },
|
56
|
+
predicate: true,
|
57
|
+
expose: :read_write
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "defines reader, writer, and predicate methods" do
|
62
|
+
described_class.define_accessor(attribute, context)
|
63
|
+
instance = dummy_class.new
|
64
|
+
|
65
|
+
expect(instance.enabled).to eq(false)
|
66
|
+
instance.enabled = true
|
67
|
+
expect(instance.enabled?).to eq(true)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe ".define_accessor!" do
|
73
|
+
let(:attribute) do
|
74
|
+
Cattri::Attribute.new(
|
75
|
+
:setting,
|
76
|
+
defined_in: dummy_class,
|
77
|
+
default: -> { "default" },
|
78
|
+
expose: :read_write
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "defines a reader/writer method" do
|
83
|
+
described_class.send(:define_accessor!, attribute, context)
|
84
|
+
instance = dummy_class.new
|
85
|
+
|
86
|
+
expect(instance.setting).to eq("default")
|
87
|
+
instance.setting("updated")
|
88
|
+
expect(instance.setting).to eq("updated")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe ".define_writer!" do
|
93
|
+
let(:attribute) do
|
94
|
+
Cattri::Attribute.new(
|
95
|
+
:mode,
|
96
|
+
defined_in: dummy_class,
|
97
|
+
default: -> { "auto" },
|
98
|
+
expose: :read_write
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "defines a writer method using 'name='" do
|
103
|
+
described_class.send(:define_accessor!, attribute, context)
|
104
|
+
described_class.send(:define_writer!, attribute, context)
|
105
|
+
|
106
|
+
instance = dummy_class.new
|
107
|
+
instance.mode = "manual"
|
108
|
+
expect(instance.mode).to eq("manual")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe ".define_predicate!" do
|
113
|
+
let(:attribute) do
|
114
|
+
Cattri::Attribute.new(
|
115
|
+
:active,
|
116
|
+
defined_in: dummy_class,
|
117
|
+
default: -> {},
|
118
|
+
predicate: true,
|
119
|
+
expose: :read_write
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "defines a predicate method returning truthiness" do
|
124
|
+
described_class.send(:define_accessor!, attribute, context)
|
125
|
+
described_class.send(:define_predicate!, attribute, context)
|
126
|
+
|
127
|
+
instance = dummy_class.new
|
128
|
+
expect(instance.active?).to be false
|
129
|
+
instance.active("yes")
|
130
|
+
expect(instance.active?).to be true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe ".memoize_default_value" do
|
135
|
+
let(:instance) { dummy_class.new }
|
136
|
+
|
137
|
+
context "non-final attribute" do
|
138
|
+
let(:attribute) do
|
139
|
+
Cattri::Attribute.new(
|
140
|
+
:foo,
|
141
|
+
defined_in: dummy_class,
|
142
|
+
default: -> { "bar" },
|
143
|
+
expose: :read_write
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "stores and returns the evaluated default" do
|
148
|
+
result = described_class.send(:memoize_default_value, instance, attribute)
|
149
|
+
expect(result).to eq("bar")
|
150
|
+
expect(instance.cattri_variable_get(:foo)).to eq("bar")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "final attribute" do
|
155
|
+
let(:attribute) do
|
156
|
+
Cattri::Attribute.new(
|
157
|
+
:immutable,
|
158
|
+
defined_in: dummy_class,
|
159
|
+
final: true,
|
160
|
+
default: -> { "locked" },
|
161
|
+
expose: :read
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "raises if value is not already set" do
|
166
|
+
expect do
|
167
|
+
described_class.send(:memoize_default_value, instance, attribute)
|
168
|
+
end.to raise_error(Cattri::AttributeError, /Final attribute :immutable cannot be written to/)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "returns value if already set" do
|
172
|
+
instance.cattri_variable_set(:immutable, "preset", final: true)
|
173
|
+
result = described_class.send(:memoize_default_value, instance, attribute)
|
174
|
+
|
175
|
+
expect(result).to eq("preset")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|