react.rb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|