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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +34 -0
  3. data/.gitignore +72 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +12 -0
  7. data/README.md +163 -151
  8. data/Steepfile +6 -0
  9. data/bin/console +33 -0
  10. data/bin/setup +8 -0
  11. data/cattri.gemspec +5 -5
  12. data/lib/cattri/attribute.rb +119 -155
  13. data/lib/cattri/attribute_compiler.rb +104 -0
  14. data/lib/cattri/attribute_options.rb +183 -0
  15. data/lib/cattri/attribute_registry.rb +155 -0
  16. data/lib/cattri/context.rb +124 -106
  17. data/lib/cattri/context_registry.rb +36 -0
  18. data/lib/cattri/deferred_attributes.rb +73 -0
  19. data/lib/cattri/dsl.rb +54 -0
  20. data/lib/cattri/error.rb +17 -90
  21. data/lib/cattri/inheritance.rb +35 -0
  22. data/lib/cattri/initializer_patch.rb +37 -0
  23. data/lib/cattri/internal_store.rb +104 -0
  24. data/lib/cattri/introspection.rb +56 -49
  25. data/lib/cattri/version.rb +3 -1
  26. data/lib/cattri.rb +38 -99
  27. data/sig/lib/cattri/attribute.rbs +105 -0
  28. data/sig/lib/cattri/attribute_compiler.rbs +61 -0
  29. data/sig/lib/cattri/attribute_options.rbs +150 -0
  30. data/sig/lib/cattri/attribute_registry.rbs +95 -0
  31. data/sig/lib/cattri/context.rbs +130 -0
  32. data/sig/lib/cattri/context_registry.rbs +31 -0
  33. data/sig/lib/cattri/deferred_attributes.rbs +53 -0
  34. data/sig/lib/cattri/dsl.rbs +55 -0
  35. data/sig/lib/cattri/error.rbs +28 -0
  36. data/sig/lib/cattri/inheritance.rbs +21 -0
  37. data/sig/lib/cattri/initializer_patch.rbs +26 -0
  38. data/sig/lib/cattri/internal_store.rbs +75 -0
  39. data/sig/lib/cattri/introspection.rbs +61 -0
  40. data/sig/lib/cattri/types.rbs +19 -0
  41. data/sig/lib/cattri/visibility.rbs +55 -0
  42. data/sig/lib/cattri.rbs +37 -0
  43. data/spec/cattri/attribute_compiler_spec.rb +179 -0
  44. data/spec/cattri/attribute_options_spec.rb +267 -0
  45. data/spec/cattri/attribute_registry_spec.rb +257 -0
  46. data/spec/cattri/attribute_spec.rb +297 -0
  47. data/spec/cattri/context_registry_spec.rb +45 -0
  48. data/spec/cattri/context_spec.rb +346 -0
  49. data/spec/cattri/deferred_attrributes_spec.rb +117 -0
  50. data/spec/cattri/dsl_spec.rb +69 -0
  51. data/spec/cattri/error_spec.rb +37 -0
  52. data/spec/cattri/inheritance_spec.rb +60 -0
  53. data/spec/cattri/initializer_patch_spec.rb +35 -0
  54. data/spec/cattri/internal_store_spec.rb +139 -0
  55. data/spec/cattri/introspection_spec.rb +90 -0
  56. data/spec/cattri/visibility_spec.rb +68 -0
  57. data/spec/cattri_spec.rb +54 -0
  58. data/spec/simplecov_helper.rb +21 -0
  59. data/spec/spec_helper.rb +16 -0
  60. metadata +79 -6
  61. data/lib/cattri/attribute_definer.rb +0 -143
  62. data/lib/cattri/class_attributes.rb +0 -277
  63. data/lib/cattri/instance_attributes.rb +0 -276
  64. data/sig/cattri.rbs +0 -4
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::DeferredAttributes do
6
+ def define_attribute(name, value, scope: :instance)
7
+ Cattri::Attribute.new(name, default: value, scope: scope, defined_in: klass)
8
+ end
9
+
10
+ let(:klass) do
11
+ Class.new do
12
+ include Cattri
13
+ extend Cattri::DeferredAttributes
14
+ end
15
+ end
16
+
17
+ let(:class_attribute) { define_attribute(:class_attr, "class", scope: :class) }
18
+ let(:instance_attribute) { define_attribute(:instance_attr, "instance") }
19
+ let(:context) { Cattri::Context.new(klass) }
20
+
21
+ describe "core methods" do
22
+ subject { klass }
23
+
24
+ before do
25
+ subject.defer_attribute(class_attribute)
26
+ subject.defer_attribute(instance_attribute)
27
+ end
28
+
29
+ describe ".extended" do
30
+ let(:base_module) { Module.new }
31
+
32
+ it "prepends Hook if not already present" do
33
+ expect(base_module.singleton_class.ancestors).not_to include(Cattri::DeferredAttributes::Hook)
34
+
35
+ described_class.extended(base_module)
36
+
37
+ expect(base_module.singleton_class.ancestors).to include(Cattri::DeferredAttributes::Hook)
38
+ end
39
+
40
+ it "does not prepend Hook twice" do
41
+ described_class.extended(base_module)
42
+ original_ancestors = base_module.singleton_class.ancestors.dup
43
+
44
+ described_class.extended(base_module)
45
+
46
+ expect(base_module.singleton_class.ancestors).to eq(original_ancestors)
47
+ end
48
+ end
49
+
50
+ describe "defer_attribute" do
51
+ it "stores deferred attributes" do
52
+ deferred_attributes = subject.instance_variable_get(:@deferred_attributes)
53
+
54
+ expect(deferred_attributes).to include(
55
+ class_attribute.name => class_attribute,
56
+ instance_attribute.name => instance_attribute
57
+ )
58
+ end
59
+ end
60
+
61
+ describe "apply_deferred_attributes" do
62
+ before do
63
+ allow(Cattri::Context).to receive(:new).and_return(context)
64
+ allow(Cattri::AttributeCompiler).to receive(:define_accessor)
65
+ end
66
+
67
+ it "applies deferred attributes to the given target" do
68
+ subject.apply_deferred_attributes(klass)
69
+
70
+ expect(Cattri::AttributeCompiler).to have_received(:define_accessor).with(class_attribute, context)
71
+ expect(Cattri::AttributeCompiler).to have_received(:define_accessor).with(instance_attribute, context)
72
+ end
73
+ end
74
+ end
75
+
76
+ describe Cattri::DeferredAttributes::Hook do
77
+ let(:klass) { Class.new }
78
+
79
+ let(:plain_module) do
80
+ Module.new.tap do |mod|
81
+ mod.singleton_class.prepend(Cattri::DeferredAttributes::Hook)
82
+ end
83
+ end
84
+
85
+ let(:hook_module) do
86
+ Module.new do
87
+ extend Cattri::DeferredAttributes
88
+
89
+ def self.apply_deferred_attributes(_target)
90
+ @called = true
91
+ end
92
+ end
93
+ end
94
+
95
+ context "with hooked module" do
96
+ it "triggers apply_deferred_attributes (include)" do
97
+ klass.include(hook_module)
98
+ expect(hook_module.instance_variable_get(:@called)).to eq(true)
99
+ end
100
+
101
+ it "triggers apply_deferred_attributes (extend)" do
102
+ klass.extend(hook_module)
103
+ expect(hook_module.instance_variable_get(:@called)).to eq(true)
104
+ end
105
+ end
106
+
107
+ context "without hooked module" do
108
+ it "does not call apply_deferred_attributes if not defined (include)" do
109
+ expect { klass.include(plain_module) }.not_to raise_error
110
+ end
111
+
112
+ it "does not call apply_deferred_attributes if not defined (extend)" do
113
+ expect { klass.extend(plain_module) }.not_to raise_error
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Cattri::Dsl do
4
+ let(:registry) { instance_double(Cattri::AttributeRegistry) }
5
+
6
+ let(:klass) do
7
+ Class.new do
8
+ extend Cattri::Dsl
9
+
10
+ def self.__cattri_visibility
11
+ :protected
12
+ end
13
+ end
14
+ end
15
+
16
+ before do
17
+ allow(klass).to receive(:attribute_registry).and_return(registry)
18
+ end
19
+
20
+ describe ".cattri" do
21
+ it "delegates to attribute_registry.define_attribute with merged visibility" do
22
+ expect(registry).to receive(:define_attribute)
23
+ .with(:foo, 123, visibility: :protected)
24
+
25
+ klass.cattri(:foo, 123)
26
+ end
27
+
28
+ it "merges user-provided options with current visibility" do
29
+ expect(registry).to receive(:define_attribute)
30
+ .with(:bar, nil, visibility: :protected, final: true)
31
+
32
+ klass.cattri(:bar, nil, final: true)
33
+ end
34
+
35
+ it "passes through block if provided" do
36
+ expect(registry).to receive(:define_attribute) do |name, value, **opts, &blk|
37
+ expect(name).to eq(:lazy)
38
+ expect(value).to be_nil
39
+ expect(opts[:visibility]).to eq(:protected)
40
+ expect(blk.call).to eq(:computed)
41
+ end
42
+
43
+ klass.cattri(:lazy) { :computed }
44
+ end
45
+ end
46
+
47
+ describe ".final_cattri" do
48
+ it "calls cattri with final: true merged into options" do
49
+ expect(klass).to receive(:cattri)
50
+ .with(:token, "xyz", hash_including(final: true))
51
+
52
+ klass.final_cattri(:token, "xyz")
53
+ end
54
+
55
+ it "respects additional options" do
56
+ expect(klass).to receive(:cattri)
57
+ .with(:token, "xyz", hash_including(scope: :class, final: true))
58
+
59
+ klass.final_cattri(:token, "xyz", scope: :class)
60
+ end
61
+
62
+ it "passes block through to cattri" do
63
+ block = proc { "lazy!" }
64
+ expect(klass).to receive(:cattri).with(:val, nil, hash_including(final: true), &block)
65
+
66
+ klass.final_cattri(:val, nil, &block)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::Error do
6
+ describe ".new" do
7
+ it "inherits from StandardError" do
8
+ expect(described_class).to be < StandardError
9
+ end
10
+
11
+ it "sets the message and custom backtrace" do
12
+ backtrace = ["custom/location.rb:42"]
13
+ error = described_class.new("oops", backtrace)
14
+
15
+ expect(error.message).to eq("oops")
16
+ expect(error.backtrace).to eq(backtrace)
17
+ end
18
+
19
+ it "defaults to caller backtrace if none given" do
20
+ error = described_class.new("default trace")
21
+ expect(error.backtrace).to be_an(Array)
22
+ expect(error.backtrace.first).to include(__FILE__)
23
+ end
24
+ end
25
+ end
26
+
27
+ RSpec.describe Cattri::AttributeError do
28
+ it "inherits from Cattri::Error" do
29
+ expect(described_class).to be < Cattri::Error
30
+ end
31
+
32
+ it "retains message and backtrace" do
33
+ error = described_class.new("bad attr", ["attr.rb:1"])
34
+ expect(error.message).to eq("bad attr")
35
+ expect(error.backtrace).to eq(["attr.rb:1"])
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::Inheritance do
6
+ let(:base_class) do
7
+ Class.new.tap do |klass|
8
+ klass.include(Cattri::ContextRegistry)
9
+ klass.singleton_class.include(Cattri::ContextRegistry)
10
+
11
+ klass.attr_reader :inherited_called_with
12
+
13
+ def klass.inherited(subclass)
14
+ @inherited_called_with = subclass
15
+ end
16
+
17
+ def klass.inherited_called_with # rubocop:disable Style/TrivialAccessors
18
+ @inherited_called_with
19
+ end
20
+ end
21
+ end
22
+
23
+ before do
24
+ allow(Cattri::Context).to receive(:new).and_call_original
25
+ allow_any_instance_of(Cattri::AttributeRegistry).to receive(:copy_attributes_to)
26
+ end
27
+
28
+ describe ".install" do
29
+ it "preserves and calls existing inherited method" do
30
+ Cattri::Inheritance.install(base_class)
31
+
32
+ subclass = Class.new(base_class) # triggers inherited hook
33
+
34
+ expect(base_class.inherited_called_with).to eq(subclass)
35
+ end
36
+
37
+ it "initializes a Cattri::Context for the subclass" do
38
+ Cattri::Inheritance.install(base_class)
39
+
40
+ expect(Cattri::Context).to receive(:new) do |actual_subclass|
41
+ expect(actual_subclass.superclass).to eq(base_class)
42
+ end
43
+
44
+ Class.new(base_class)
45
+ end
46
+
47
+ it "invokes copy_attributes_to with the subclass context" do
48
+ context = instance_double(Cattri::Context)
49
+ allow(Cattri::Context).to receive(:new).and_return(context)
50
+
51
+ registry = instance_double(Cattri::AttributeRegistry)
52
+ allow(base_class).to receive(:attribute_registry).and_return(registry)
53
+
54
+ expect(registry).to receive(:copy_attributes_to).with(context)
55
+
56
+ Cattri::Inheritance.install(base_class)
57
+ Class.new(base_class)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::InitializerPatch do
6
+ let(:klass) do
7
+ Class.new do
8
+ include Cattri
9
+
10
+ cattri :final_attr, -> { "default-final" }, final: true
11
+ cattri :regular_attr, -> { "default-regular" }
12
+
13
+ def initialize(preset: nil)
14
+ self.final_attr = preset if preset
15
+ end
16
+ end
17
+ end
18
+
19
+ subject(:instance) { klass.new }
20
+
21
+ describe "#initialize (via InitializerPatch)" do
22
+ it "sets the default value for final attribute if unset" do
23
+ expect(instance.final_attr).to eq("default-final")
24
+ end
25
+
26
+ it "does not override a final value if already set in initialize" do
27
+ custom = klass.new(preset: "explicit")
28
+ expect(custom.final_attr).to eq("explicit")
29
+ end
30
+
31
+ it "does not set default for non-final attributes" do
32
+ expect(instance.cattri_variable_defined?(:regular_attr)).to be false
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::InternalStore do
6
+ let(:klass) do
7
+ Class.new do
8
+ include Cattri::InternalStore
9
+
10
+ public :normalize_ivar, :__cattri_store, :__cattri_set_variables
11
+ end
12
+ end
13
+
14
+ subject(:instance) { klass.new }
15
+
16
+ describe "#cattri_variable_defined?" do
17
+ it "returns false when variable is not set" do
18
+ expect(instance.cattri_variable_defined?(:foo)).to be false
19
+ end
20
+
21
+ it "returns true when variable is set" do
22
+ instance.cattri_variable_set(:foo, 123)
23
+ expect(instance.cattri_variable_defined?(:foo)).to be true
24
+ end
25
+ end
26
+
27
+ describe "#cattri_variable_get" do
28
+ it "returns nil when variable is not set" do
29
+ expect(instance.cattri_variable_get(:bar)).to be_nil
30
+ end
31
+
32
+ it "returns the value when variable is set" do
33
+ instance.cattri_variable_set(:bar, "hello")
34
+ expect(instance.cattri_variable_get(:bar)).to eq("hello")
35
+ end
36
+ end
37
+
38
+ describe "#cattri_variable_set" do
39
+ it "sets the value for a variable" do
40
+ instance.cattri_variable_set(:baz, 42)
41
+ expect(instance.cattri_variable_get(:baz)).to eq(42)
42
+ end
43
+
44
+ it "tracks the variable as explicitly set" do
45
+ instance.cattri_variable_set(:baz, 42)
46
+ expect(instance.__cattri_set_variables).to include(:baz)
47
+ end
48
+
49
+ it "normalizes the key (removes @ and converts to symbol)" do
50
+ instance.cattri_variable_set("@qux", 99)
51
+ expect(instance.__cattri_store).to have_key(:qux)
52
+ end
53
+
54
+ it "raises if modifying a final value" do
55
+ instance.cattri_variable_set(:locked, "a", final: true)
56
+
57
+ expect do
58
+ instance.cattri_variable_set(:locked, "b", final: true)
59
+ end.to raise_error(Cattri::AttributeError, /Cannot modify final attribute :locked/)
60
+ end
61
+ end
62
+
63
+ describe "#cattri_variable_memoize" do
64
+ it "memoizes value if not already set" do
65
+ result = instance.cattri_variable_memoize(:lazy) { "computed" }
66
+
67
+ expect(result).to eq("computed")
68
+ expect(instance.cattri_variable_get(:lazy)).to eq("computed")
69
+ end
70
+
71
+ it "returns existing value if already set" do
72
+ instance.cattri_variable_set(:lazy, "preset")
73
+ result = instance.cattri_variable_memoize(:lazy) { "should not run" }
74
+
75
+ expect(result).to eq("preset")
76
+ end
77
+ end
78
+
79
+ describe "#__cattri_store" do
80
+ it "returns a Hash" do
81
+ expect(instance.__cattri_store).to be_a(Hash)
82
+ end
83
+
84
+ it "memoizes the store" do
85
+ expect(instance.__cattri_store).to equal(instance.__cattri_store)
86
+ end
87
+ end
88
+
89
+ describe "#__cattri_set_variables" do
90
+ it "returns a Set" do
91
+ expect(instance.__cattri_set_variables).to be_a(Set)
92
+ end
93
+
94
+ it "memoizes the set" do
95
+ expect(instance.__cattri_set_variables).to equal(instance.__cattri_set_variables)
96
+ end
97
+ end
98
+
99
+ describe "#normalize_ivar" do
100
+ it "converts string with @ to symbol without @" do
101
+ expect(instance.normalize_ivar("@name")).to eq(:name)
102
+ end
103
+
104
+ it "converts plain string to symbol" do
105
+ expect(instance.normalize_ivar("email")).to eq(:email)
106
+ end
107
+
108
+ it "converts symbol to symbol" do
109
+ expect(instance.normalize_ivar(:age)).to eq(:age)
110
+ end
111
+
112
+ it "freezes the returned symbol" do
113
+ sym = instance.normalize_ivar("cool")
114
+ expect(sym).to be_frozen
115
+ end
116
+ end
117
+
118
+ describe "#guard_final!" do
119
+ it "raises if key is defined and final" do
120
+ instance.__cattri_store[:foo] = Cattri::AttributeValue.new("sealed", true)
121
+ expect do
122
+ instance.send(:guard_final!, :foo)
123
+ end.to raise_error(Cattri::AttributeError, /Cannot modify final attribute :foo/)
124
+ end
125
+
126
+ it "does nothing if key is not defined" do
127
+ expect do
128
+ instance.send(:guard_final!, :bar)
129
+ end.not_to raise_error
130
+ end
131
+
132
+ it "does nothing if key is defined but not final" do
133
+ instance.__cattri_store[:baz] = Cattri::AttributeValue.new("open", false)
134
+ expect do
135
+ instance.send(:guard_final!, :baz)
136
+ end.not_to raise_error
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri::Introspection do
6
+ let(:klass) do
7
+ Class.new do
8
+ include Cattri
9
+ cattri :foo, 123
10
+ end
11
+ end
12
+
13
+ let(:subclass) do
14
+ Class.new(klass) do
15
+ include Cattri::Introspection
16
+ cattri :bar, "bar"
17
+ end
18
+ end
19
+
20
+ before do
21
+ klass.include(Cattri::Introspection)
22
+ end
23
+
24
+ describe ".attribute_defined?" do
25
+ it "returns true for defined attributes" do
26
+ expect(klass.attribute_defined?(:foo)).to be true
27
+ end
28
+
29
+ it "returns false for unknown attributes" do
30
+ expect(klass.attribute_defined?(:missing)).to be false
31
+ end
32
+ end
33
+
34
+ describe ".attribute" do
35
+ it "returns the attribute definition" do
36
+ attr = klass.attribute(:foo)
37
+ expect(attr).to be_a(Cattri::Attribute)
38
+ expect(attr.name).to eq(:foo)
39
+ end
40
+
41
+ it "returns nil for unknown attributes" do
42
+ expect(klass.attribute(:missing)).to be_nil
43
+ end
44
+ end
45
+
46
+ describe ".attributes" do
47
+ it "lists local attributes by default" do
48
+ expect(subclass.attributes).to eq([:bar])
49
+ end
50
+
51
+ it "includes inherited attributes when requested" do
52
+ expect(subclass.attributes(with_ancestors: true)).to match_array(%i[foo bar])
53
+ end
54
+ end
55
+
56
+ describe ".attribute_definitions" do
57
+ it "returns a hash of defined attributes" do
58
+ defs = subclass.attribute_definitions
59
+ expect(defs).to be_a(Hash)
60
+ expect(defs).to have_key(:bar)
61
+ expect(defs[:bar]).to be_a(Cattri::Attribute)
62
+ end
63
+
64
+ it "includes ancestors if requested" do
65
+ defs = subclass.attribute_definitions(with_ancestors: true)
66
+ expect(defs).to have_key(:foo)
67
+ expect(defs[:foo]).to be_a(Cattri::Attribute)
68
+ end
69
+ end
70
+
71
+ describe ".attribute_methods" do
72
+ it "returns a hash of methods per attribute" do
73
+ methods = subclass.attribute_methods
74
+ expect(methods).to be_a(Hash)
75
+ expect(methods.keys).to include(:bar)
76
+ expect(methods[:bar]).to include(:bar)
77
+ end
78
+ end
79
+
80
+ describe ".attribute_source" do
81
+ it "returns the class where the attribute was originally defined" do
82
+ expect(subclass.attribute_source(:foo)).to eq(klass)
83
+ expect(subclass.attribute_source(:bar)).to eq(subclass)
84
+ end
85
+
86
+ it "returns nil for unknown attributes" do
87
+ expect(subclass.attribute_source(:missing)).to be_nil
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "cattri/visibility"
5
+
6
+ RSpec.describe Cattri::Visibility do
7
+ let(:klass) do
8
+ Class.new do
9
+ extend Cattri::Visibility
10
+ end
11
+ end
12
+
13
+ describe "#__cattri_visibility" do
14
+ it "defaults to public" do
15
+ expect(klass.send(:__cattri_visibility)).to eq(:public)
16
+ end
17
+ end
18
+
19
+ describe "#public" do
20
+ it "sets visibility to public when no args are passed" do
21
+ klass.protected
22
+ klass.public
23
+
24
+ expect(klass.send(:__cattri_visibility)).to eq(:public)
25
+ end
26
+
27
+ it "delegates to Module#public when args are passed" do
28
+ klass.class_eval do
29
+ def sample_method; end
30
+ end
31
+
32
+ expect { klass.public :sample_method }.not_to raise_error
33
+ expect(klass.public_instance_methods).to include(:sample_method)
34
+ end
35
+ end
36
+
37
+ describe "#protected" do
38
+ it "sets visibility to protected when no args are passed" do
39
+ klass.protected
40
+ expect(klass.send(:__cattri_visibility)).to eq(:protected)
41
+ end
42
+
43
+ it "delegates to Module#protected when args are passed" do
44
+ klass.class_eval do
45
+ def protected_method; end
46
+ end
47
+
48
+ expect { klass.protected :protected_method }.not_to raise_error
49
+ expect(klass.protected_instance_methods).to include(:protected_method)
50
+ end
51
+ end
52
+
53
+ describe "#private" do
54
+ it "sets visibility to private when no args are passed" do
55
+ klass.private
56
+ expect(klass.send(:__cattri_visibility)).to eq(:private)
57
+ end
58
+
59
+ it "delegates to Module#private when args are passed" do
60
+ klass.class_eval do
61
+ def private_method; end
62
+ end
63
+
64
+ expect { klass.private :private_method }.not_to raise_error
65
+ expect(klass.private_instance_methods).to include(:private_method)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Cattri do
6
+ let(:klass) do
7
+ Class.new do
8
+ include Cattri
9
+
10
+ cattri :token, -> { "abc123" }
11
+ cattri :final_token, -> { "321cba" }, final: true
12
+ end
13
+ end
14
+
15
+ subject(:instance) { klass.new }
16
+
17
+ describe "including Cattri" do
18
+ it "defines a working accessor" do
19
+ expect(instance.token).to eq("abc123")
20
+ instance.token = "xyz"
21
+ expect(instance.token).to eq("xyz")
22
+ end
23
+
24
+ it "respects default visibility (public)" do
25
+ expect(klass.public_instance_methods).to include(:token, :token=)
26
+ end
27
+
28
+ it "installs initializer patch for default assignment" do
29
+ expect(instance.cattri_variable_defined?(:final_token)).to be true
30
+ expect(instance.cattri_variable_get(:final_token)).to eq("321cba")
31
+ end
32
+ end
33
+
34
+ describe ".with_cattri_introspection" do
35
+ let(:introspective_class) do
36
+ Class.new do
37
+ include Cattri
38
+ cattri :id, "foo"
39
+
40
+ with_cattri_introspection
41
+ end
42
+ end
43
+
44
+ it "adds .attribute_defined?" do
45
+ expect(introspective_class.attribute_defined?(:id)).to be true
46
+ expect(introspective_class.attribute_defined?(:missing)).to be false
47
+ end
48
+
49
+ it "returns attribute source and methods" do
50
+ expect(introspective_class.attribute_source(:id)).to eq(introspective_class)
51
+ expect(introspective_class.attribute_methods).to include(:id)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+ require "simplecov-cobertura"
5
+ require "simplecov-html"
6
+
7
+ SimpleCov.formatters = [
8
+ SimpleCov::Formatter::CoberturaFormatter,
9
+ SimpleCov::Formatter::HTMLFormatter
10
+ ]
11
+
12
+ SimpleCov.start do
13
+ enable_coverage :branch
14
+
15
+ track_files "lib/cattri/**/*.rb"
16
+
17
+ add_filter "lib/cattri/version.rb"
18
+ add_filter "/spec/"
19
+ end
20
+
21
+ SimpleCov.minimum_coverage 100