react.rb 0.0.1
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 +7 -0
- data/.gitignore +30 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +19 -0
- data/README.md +166 -0
- data/config.ru +15 -0
- data/example/react-tutorial/Gemfile +6 -0
- data/example/react-tutorial/Gemfile.lock +45 -0
- data/example/react-tutorial/README.md +8 -0
- data/example/react-tutorial/_comments.json +6 -0
- data/example/react-tutorial/config.ru +55 -0
- data/example/react-tutorial/example.rb +104 -0
- data/example/react-tutorial/public/base.css +62 -0
- data/example/todos/Gemfile +11 -0
- data/example/todos/Gemfile.lock +84 -0
- data/example/todos/README.md +37 -0
- data/example/todos/Rakefile +8 -0
- data/example/todos/app/application.rb +22 -0
- data/example/todos/app/components/app.react.rb +61 -0
- data/example/todos/app/components/footer.react.rb +31 -0
- data/example/todos/app/components/todo_item.react.rb +46 -0
- data/example/todos/app/components/todo_list.react.rb +25 -0
- data/example/todos/app/models/todo.rb +19 -0
- data/example/todos/config.ru +14 -0
- data/example/todos/index.html.haml +16 -0
- data/example/todos/spec/todo_spec.rb +28 -0
- data/example/todos/vendor/base.css +410 -0
- data/example/todos/vendor/bg.png +0 -0
- data/example/todos/vendor/jquery.js +4 -0
- data/lib/react.rb +16 -0
- data/lib/react/api.rb +95 -0
- data/lib/react/callbacks.rb +35 -0
- data/lib/react/component.rb +197 -0
- data/lib/react/element.rb +63 -0
- data/lib/react/event.rb +76 -0
- data/lib/react/ext/hash.rb +9 -0
- data/lib/react/ext/string.rb +8 -0
- data/lib/react/top_level.rb +50 -0
- data/lib/react/validator.rb +65 -0
- data/lib/react/version.rb +3 -0
- data/react.rb.gemspec +24 -0
- data/spec/callbacks_spec.rb +107 -0
- data/spec/component_spec.rb +556 -0
- data/spec/element_spec.rb +60 -0
- data/spec/event_spec.rb +22 -0
- data/spec/react_spec.rb +168 -0
- data/spec/reactjs/index.html.erb +10 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/validator_spec.rb +79 -0
- data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
- data/vendor/active_support/core_ext/class/attribute.rb +127 -0
- data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
- data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
- metadata +189 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe React::Element do
|
4
|
+
it "should bridge `type` of native React.Element attributes" do
|
5
|
+
element = React.create_element('div')
|
6
|
+
expect(element.element_type).to eq("div")
|
7
|
+
end
|
8
|
+
|
9
|
+
async "should be renderable" do
|
10
|
+
element = React.create_element('span')
|
11
|
+
div = `document.createElement("div")`
|
12
|
+
React.render(element, div) do
|
13
|
+
run_async {
|
14
|
+
expect(`div.children[0].tagName`).to eq("SPAN")
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "Event subscription" do
|
20
|
+
it "should be subscribable through `on(:event_name)` method" do
|
21
|
+
expect { |b|
|
22
|
+
element = React.create_element("div").on(:click, &b)
|
23
|
+
instance = renderElementToDocument(element)
|
24
|
+
simulateEvent(:click, instance)
|
25
|
+
}.to yield_with_args(React::Event)
|
26
|
+
|
27
|
+
expect { |b|
|
28
|
+
element = React.create_element("div").on(:key_down, &b)
|
29
|
+
instance = renderElementToDocument(element)
|
30
|
+
simulateEvent(:keyDown, instance, {key: "Enter"})
|
31
|
+
}.to yield_control
|
32
|
+
|
33
|
+
expect { |b|
|
34
|
+
element = React.create_element("form").on(:submit, &b)
|
35
|
+
instance = renderElementToDocument(element)
|
36
|
+
simulateEvent(:submit, instance, {})
|
37
|
+
}.to yield_control
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return self for `on` method" do
|
41
|
+
element = React.create_element("div")
|
42
|
+
expect(element.on(:click){}).to eq(element)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "Children" do
|
47
|
+
it "should return a Enumerable" do
|
48
|
+
ele = React.create_element('div') { [React.create_element('a'), React.create_element('li')] }
|
49
|
+
nodes = ele.children.map {|ele| ele.element_type }
|
50
|
+
expect(nodes).to eq(["a", "li"])
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should return a Enumerator when not providing a block" do
|
54
|
+
ele = React.create_element('div') { [React.create_element('a'), React.create_element('li')] }
|
55
|
+
nodes = ele.children.each
|
56
|
+
expect(nodes).to be_a(Enumerator)
|
57
|
+
expect(nodes.size).to eq(2)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/event_spec.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe React::Event do
|
4
|
+
it "should bridge attributes of native SyntheticEvent (see http://facebook.github.io/react/docs/events.html#syntheticevent)" do
|
5
|
+
element = React.create_element('div').on(:click) do |event|
|
6
|
+
expect(event.bubbles).to eq(`#{event.to_n}.bubbles`)
|
7
|
+
expect(event.cancelable).to eq(`#{event.to_n}.cancelable`)
|
8
|
+
expect(event.current_target).to eq(`#{event.to_n}.currentTarget`)
|
9
|
+
expect(event.default_prevented).to eq(`#{event.to_n}.defaultPrevented`)
|
10
|
+
expect(event.event_phase).to eq(`#{event.to_n}.eventPhase`)
|
11
|
+
expect(event.is_trusted?).to eq(`#{event.to_n}.isTrusted`)
|
12
|
+
expect(event.native_event).to eq(`#{event.to_n}.nativeEvent`)
|
13
|
+
expect(event.target).to eq(`#{event.to_n}.target`)
|
14
|
+
expect(event.timestamp).to eq(`#{event.to_n}.timeStamp`)
|
15
|
+
expect(event.event_type).to eq(`#{event.to_n}.type`)
|
16
|
+
expect(event).to respond_to(:prevent_default)
|
17
|
+
expect(event).to respond_to(:stop_propagation)
|
18
|
+
end
|
19
|
+
instance = renderElementToDocument(element)
|
20
|
+
simulateEvent(:click, instance)
|
21
|
+
end
|
22
|
+
end
|
data/spec/react_spec.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe React do
|
4
|
+
describe "is_valid_element" do
|
5
|
+
it "should return true if passed a valid element" do
|
6
|
+
element = React::Element.new(`React.createElement('div')`)
|
7
|
+
expect(React.is_valid_element(element)).to eq(true)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return false is passed a non React element" do
|
11
|
+
element = React::Element.new(`{}`)
|
12
|
+
expect(React.is_valid_element(element)).to eq(false)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "create_element" do
|
17
|
+
it "should create a valid element with only tag" do
|
18
|
+
element = React.create_element('div')
|
19
|
+
expect(React.is_valid_element(element)).to eq(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with block" do
|
23
|
+
it "should create a valid element with text as only child when block yield String" do
|
24
|
+
element = React.create_element('div') { "lorem ipsum" }
|
25
|
+
expect(React.is_valid_element(element)).to eq(true)
|
26
|
+
expect(element.props.children).to eq("lorem ipsum")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should create a valid element with children as array when block yield Array of element" do
|
30
|
+
element = React.create_element('div') do
|
31
|
+
[React.create_element('span'), React.create_element('span'), React.create_element('span')]
|
32
|
+
end
|
33
|
+
expect(React.is_valid_element(element)).to eq(true)
|
34
|
+
expect(element.props.children.length).to eq(3)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should render element with children as array when block yield Array of element" do
|
38
|
+
element = React.create_element('div') do
|
39
|
+
[React.create_element('span'), React.create_element('span'), React.create_element('span')]
|
40
|
+
end
|
41
|
+
instance = renderElementToDocument(element)
|
42
|
+
expect(instance.getDOMNode.childNodes.length).to eq(3)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
describe "custom element" do
|
46
|
+
before do
|
47
|
+
stub_const 'Foo', Class.new
|
48
|
+
Foo.class_eval do
|
49
|
+
def render
|
50
|
+
React.create_element("div") { "lorem" }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should render element with only one children correctly" do
|
56
|
+
element = React.create_element(Foo) { React.create_element('span') }
|
57
|
+
instance = renderElementToDocument(element)
|
58
|
+
expect(instance.props.children).not_to be_a(Array)
|
59
|
+
expect(instance.props.children.type).to eq("span")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should render element with more than one children correctly" do
|
63
|
+
element = React.create_element(Foo) { [React.create_element('span'), React.create_element('span')] }
|
64
|
+
instance = renderElementToDocument(element)
|
65
|
+
expect(instance.props.children).to be_a(Array)
|
66
|
+
expect(instance.props.children.length).to eq(2)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should create a valid element provided class defined `render`" do
|
70
|
+
element = React.create_element(Foo)
|
71
|
+
expect(React.is_valid_element(element)).to eq(true)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should allow creating with properties" do
|
75
|
+
element = React.create_element(Foo, foo: "bar")
|
76
|
+
expect(element.props.foo).to eq("bar")
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should raise error if provided class doesn't defined `render`" do
|
80
|
+
expect { React.create_element(Array) }.to raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "create element with properties" do
|
85
|
+
it "should enforce snake-cased property name" do
|
86
|
+
element = React.create_element("div", class_name: "foo")
|
87
|
+
expect(element.props.className).to eq("foo")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should allow custom property" do
|
91
|
+
element = React.create_element("div", foo: "bar")
|
92
|
+
expect(element.props.foo).to eq("bar")
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should not camel-case custom property" do
|
96
|
+
element = React.create_element("div", foo_bar: "foo")
|
97
|
+
expect(element.props.foo_bar).to eq("foo")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "class_name helpers (React.addons.classSet)" do
|
102
|
+
it "should transform Hash provided to `class_name` props as string" do
|
103
|
+
classes = {foo: true, bar: false, lorem: true}
|
104
|
+
element = React.create_element("div", class_name: classes)
|
105
|
+
|
106
|
+
expect(element.props.className).to eq("foo lorem")
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should not alter behavior when passing a string" do
|
110
|
+
element = React.create_element("div", class_name: "foo bar")
|
111
|
+
|
112
|
+
expect(element.props.className).to eq("foo bar")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
describe "render" do
|
119
|
+
async "should render element to DOM" do
|
120
|
+
div = `document.createElement("div")`
|
121
|
+
React.render(React.create_element('span') { "lorem" }, div) do
|
122
|
+
run_async {
|
123
|
+
expect(`div.children[0].tagName`).to eq("SPAN")
|
124
|
+
expect(`div.textContent`).to eq("lorem")
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should work without providing a block" do
|
130
|
+
div = `document.createElement("div")`
|
131
|
+
React.render(React.create_element('span') { "lorem" }, div)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should return a React::Component::API compatible object" do
|
135
|
+
div = `document.createElement("div")`
|
136
|
+
component = React.render(React.create_element('span') { "lorem" }, div)
|
137
|
+
React::Component::API.public_instance_methods(true).each do |method_name|
|
138
|
+
expect(component).to respond_to(method_name)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
pending "should return nil to prevent abstraction leakage" do
|
143
|
+
div = `document.createElement("div")`
|
144
|
+
expect {
|
145
|
+
React.render(React.create_element('span') { "lorem" }, div)
|
146
|
+
}.to be_nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "render_to_string" do
|
151
|
+
it "should render a React.Element to string" do
|
152
|
+
ele = React.create_element('span') { "lorem" }
|
153
|
+
expect(React.render_to_string(ele)).to be_kind_of(String)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "unmount_component_at_node" do
|
158
|
+
async "should unmount component at node" do
|
159
|
+
div = `document.createElement("div")`
|
160
|
+
React.render(React.create_element('span') { "lorem" }, div ) do
|
161
|
+
run_async {
|
162
|
+
expect(React.unmount_component_at_node(div)).to eq(true)
|
163
|
+
}
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'react'
|
2
|
+
|
3
|
+
module ReactTestHelpers
|
4
|
+
`var ReactTestUtils = React.addons.TestUtils`
|
5
|
+
|
6
|
+
def renderToDocument(type, options = {})
|
7
|
+
element = React.create_element(type, options)
|
8
|
+
return renderElementToDocument(element)
|
9
|
+
end
|
10
|
+
|
11
|
+
def renderElementToDocument(element)
|
12
|
+
instance = Native(`ReactTestUtils.renderIntoDocument(#{element.to_n})`)
|
13
|
+
instance.class.include(React::Component::API)
|
14
|
+
return instance
|
15
|
+
end
|
16
|
+
|
17
|
+
def simulateEvent(event, element, params = {})
|
18
|
+
simulator = Native(`ReactTestUtils.Simulate`)
|
19
|
+
simulator[event.to_s].call(`#{element.to_n}.getDOMNode()`, params)
|
20
|
+
end
|
21
|
+
|
22
|
+
def isElementOfType(element, type)
|
23
|
+
`React.addons.TestUtils.isElementOfType(#{element.to_n}, #{type.cached_component_class})`
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec.configure do |config|
|
28
|
+
config.include ReactTestHelpers
|
29
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe React::Validator do
|
4
|
+
describe "validate" do
|
5
|
+
describe "Presence validation" do
|
6
|
+
it "should check if required props provided" do
|
7
|
+
validator = React::Validator.build do
|
8
|
+
requires :foo
|
9
|
+
requires :bar
|
10
|
+
end
|
11
|
+
|
12
|
+
expect(validator.validate({})).to eq(["Required prop `foo` was not specified", "Required prop `bar` was not specified"])
|
13
|
+
expect(validator.validate({foo: 1, bar: 3})).to eq([])
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should check if passed non specified prop" do
|
17
|
+
validator = React::Validator.build do
|
18
|
+
optional :foo
|
19
|
+
end
|
20
|
+
|
21
|
+
expect(validator.validate({bar: 10})).to eq(["Provided prop `bar` not specified in spec"])
|
22
|
+
expect(validator.validate({foo: 10})).to eq([])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "Type validation" do
|
27
|
+
it "should check if passed value with wrong type" do
|
28
|
+
validator = React::Validator.build do
|
29
|
+
requires :foo, type: String
|
30
|
+
end
|
31
|
+
|
32
|
+
expect(validator.validate({foo: 10})).to eq(["Provided prop `foo` was not the specified type `String`"])
|
33
|
+
expect(validator.validate({foo: "10"})).to eq([])
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should check if passed value with wrong custom type" do
|
37
|
+
stub_const 'Bar', Class.new
|
38
|
+
validator = React::Validator.build do
|
39
|
+
requires :foo, type: Bar
|
40
|
+
end
|
41
|
+
|
42
|
+
expect(validator.validate({foo: 10})).to eq(["Provided prop `foo` was not the specified type `Bar`"])
|
43
|
+
expect(validator.validate({foo: Bar.new})).to eq([])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should support Array[Class] validation" do
|
47
|
+
validator = React::Validator.build do
|
48
|
+
requires :foo, type: Array[Hash]
|
49
|
+
end
|
50
|
+
|
51
|
+
expect(validator.validate({foo: [1,'2',3]})).to eq(["Provided prop `foo` was not an Array of the specified type `Hash`"])
|
52
|
+
expect(validator.validate({foo: [{},{},{}]})).to eq([])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "Limited values" do
|
57
|
+
it "should check if passed value is not one of the specified values" do
|
58
|
+
validator = React::Validator.build do
|
59
|
+
requires :foo, values: [4,5,6]
|
60
|
+
end
|
61
|
+
|
62
|
+
expect(validator.validate({foo: 3})).to eq(["Value `3` for prop `foo` is not an allowed value"])
|
63
|
+
expect(validator.validate({foo: 4})).to eq([])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "default_props" do
|
69
|
+
it "should return specified default values" do
|
70
|
+
validator = React::Validator.build do
|
71
|
+
requires :foo, default: 10
|
72
|
+
requires :bar
|
73
|
+
optional :lorem, default: 20
|
74
|
+
end
|
75
|
+
|
76
|
+
expect(validator.default_props).to eq({foo: 10, lorem: 20})
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Hash
|
2
|
+
# By default, only instances of Hash itself are extractable.
|
3
|
+
# Subclasses of Hash may implement this method and return
|
4
|
+
# true to declare themselves as extractable. If a Hash
|
5
|
+
# is extractable, Array#extract_options! pops it from
|
6
|
+
# the Array when it is the last element of the Array.
|
7
|
+
def extractable_options?
|
8
|
+
instance_of?(Hash)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Array
|
13
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
14
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
15
|
+
#
|
16
|
+
# def options(*args)
|
17
|
+
# args.extract_options!
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# options(1, 2) # => {}
|
21
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
22
|
+
def extract_options!
|
23
|
+
if last.is_a?(Hash) && last.extractable_options?
|
24
|
+
pop
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'active_support/core_ext/kernel/singleton_class'
|
2
|
+
require 'active_support/core_ext/module/remove_method'
|
3
|
+
require 'active_support/core_ext/array/extract_options'
|
4
|
+
|
5
|
+
class Class
|
6
|
+
# Declare a class-level attribute whose value is inheritable by subclasses.
|
7
|
+
# Subclasses can change their own value and it will not impact parent class.
|
8
|
+
#
|
9
|
+
# class Base
|
10
|
+
# class_attribute :setting
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class Subclass < Base
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Base.setting = true
|
17
|
+
# Subclass.setting # => true
|
18
|
+
# Subclass.setting = false
|
19
|
+
# Subclass.setting # => false
|
20
|
+
# Base.setting # => true
|
21
|
+
#
|
22
|
+
# In the above case as long as Subclass does not assign a value to setting
|
23
|
+
# by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
|
24
|
+
# would read value assigned to parent class. Once Subclass assigns a value then
|
25
|
+
# the value assigned by Subclass would be returned.
|
26
|
+
#
|
27
|
+
# This matches normal Ruby method inheritance: think of writing an attribute
|
28
|
+
# on a subclass as overriding the reader method. However, you need to be aware
|
29
|
+
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
30
|
+
# In such cases, you don't want to do changes in places but use setters:
|
31
|
+
#
|
32
|
+
# Base.setting = []
|
33
|
+
# Base.setting # => []
|
34
|
+
# Subclass.setting # => []
|
35
|
+
#
|
36
|
+
# # Appending in child changes both parent and child because it is the same object:
|
37
|
+
# Subclass.setting << :foo
|
38
|
+
# Base.setting # => [:foo]
|
39
|
+
# Subclass.setting # => [:foo]
|
40
|
+
#
|
41
|
+
# # Use setters to not propagate changes:
|
42
|
+
# Base.setting = []
|
43
|
+
# Subclass.setting += [:foo]
|
44
|
+
# Base.setting # => []
|
45
|
+
# Subclass.setting # => [:foo]
|
46
|
+
#
|
47
|
+
# For convenience, an instance predicate method is defined as well.
|
48
|
+
# To skip it, pass <tt>instance_predicate: false</tt>.
|
49
|
+
#
|
50
|
+
# Subclass.setting? # => false
|
51
|
+
#
|
52
|
+
# Instances may overwrite the class value in the same way:
|
53
|
+
#
|
54
|
+
# Base.setting = true
|
55
|
+
# object = Base.new
|
56
|
+
# object.setting # => true
|
57
|
+
# object.setting = false
|
58
|
+
# object.setting # => false
|
59
|
+
# Base.setting # => true
|
60
|
+
#
|
61
|
+
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
|
62
|
+
#
|
63
|
+
# object.setting # => NoMethodError
|
64
|
+
# object.setting? # => NoMethodError
|
65
|
+
#
|
66
|
+
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
|
67
|
+
#
|
68
|
+
# object.setting = false # => NoMethodError
|
69
|
+
#
|
70
|
+
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
|
71
|
+
def class_attribute(*attrs)
|
72
|
+
options = attrs.extract_options!
|
73
|
+
instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
|
74
|
+
instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
|
75
|
+
instance_predicate = options.fetch(:instance_predicate, true)
|
76
|
+
|
77
|
+
attrs.each do |name|
|
78
|
+
define_singleton_method(name) { nil }
|
79
|
+
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
|
80
|
+
|
81
|
+
ivar = "@#{name}"
|
82
|
+
|
83
|
+
define_singleton_method("#{name}=") do |val|
|
84
|
+
singleton_class.class_eval do
|
85
|
+
remove_possible_method(name)
|
86
|
+
define_method(name) { val }
|
87
|
+
end
|
88
|
+
|
89
|
+
if singleton_class?
|
90
|
+
class_eval do
|
91
|
+
remove_possible_method(name)
|
92
|
+
define_method(name) do
|
93
|
+
if instance_variable_defined? ivar
|
94
|
+
instance_variable_get ivar
|
95
|
+
else
|
96
|
+
singleton_class.send name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
val
|
102
|
+
end
|
103
|
+
|
104
|
+
if instance_reader
|
105
|
+
remove_possible_method name
|
106
|
+
define_method(name) do
|
107
|
+
if instance_variable_defined?(ivar)
|
108
|
+
instance_variable_get ivar
|
109
|
+
else
|
110
|
+
self.class.public_send name
|
111
|
+
end
|
112
|
+
end
|
113
|
+
define_method("#{name}?") { !!public_send(name) } if instance_predicate
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_writer name if instance_writer
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
unless respond_to?(:singleton_class?)
|
123
|
+
def singleton_class?
|
124
|
+
ancestors.first != self
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|