js_objects 0.6.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.
- data/README.md +251 -0
- data/lib/js_object.rb +63 -0
- data/lib/js_objects.rb +2 -0
- data/lib/prototype.rb +66 -0
- metadata +51 -0
data/README.md
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# Javascript Objects Gem
|
2
|
+
## About
|
3
|
+
The not-so-creative name pretty much gives it all away. This gem allows you to use
|
4
|
+
a Javascript like object in Ruby. Some documentation as well as what I assume will
|
5
|
+
be frequently asked questions are below.
|
6
|
+
## Dependencies
|
7
|
+
Ruby 1.9 or Ruby 2.0. Might work on 1.8.7, but I haven't checked.
|
8
|
+
## How to install
|
9
|
+
Y'know, the way you normally install gems. Either: ```gem install js_objects``` or
|
10
|
+
add ```gem js_objects``` to your Gemfile and ```bundle install```.
|
11
|
+
## JsObject
|
12
|
+
JsObjects are supposed to behave like Javascript objects, meaning you can define methods
|
13
|
+
and attributes on them through a ```#["attribute"]```, calling ```#attribute=``` (where 'attribute'
|
14
|
+
is mostly whatever you want it to be). They also implement a prototypal inheritance scheme, just
|
15
|
+
like real Javascript. That means they have a prototype (either another JsObject or an instance of
|
16
|
+
the Prototype class) that you can modify or set yourself. JsObject inherits from Hash, so it has
|
17
|
+
all of the Hash methods available to it. Not all of these methods are wrapped up in by this code
|
18
|
+
so it's possible you could experience some weird behavior or get your object into a weird state.
|
19
|
+
But that's all of programming though.
|
20
|
+
### Basics
|
21
|
+
First, get a Javascript object like so:
|
22
|
+
```ruby
|
23
|
+
js_obj = JsObject.new
|
24
|
+
```
|
25
|
+
You can also give it an argument if you don't want to use its default prototype (more on prototypes later).
|
26
|
+
```ruby
|
27
|
+
JsObject.new(prototype)
|
28
|
+
```
|
29
|
+
Once you have your JS object, you can treat it pretty much like... well, an object in Javascript.
|
30
|
+
```ruby
|
31
|
+
js_obj.something = "something"
|
32
|
+
js_obj.something # => "something"
|
33
|
+
js_obj["something"] # => "something"
|
34
|
+
js_obj[:something] # => "something"
|
35
|
+
|
36
|
+
js_obj[:false] = false
|
37
|
+
js_obj.false # => false
|
38
|
+
|
39
|
+
js_obj["nil"] = nil
|
40
|
+
js_obj[:nil] # => nil
|
41
|
+
```
|
42
|
+
Call a method for an attribute that hasn't been defined yet and you'll get back ```nil``` (unless
|
43
|
+
you do some stuff with the prototype, but again, more on that later).
|
44
|
+
```ruby
|
45
|
+
js_obj.never_defined # => nil
|
46
|
+
```
|
47
|
+
### Indifference
|
48
|
+
JsObjects are _extremely_ indifferent. Strings, symbols, numbers; they're all the same to your JsObject.
|
49
|
+
When you do use numbers as keys, remember that you can't later call ```.5``` on JsObjects or any other object for that
|
50
|
+
matter. Ruby doesn't like it. It also doesn't like ```:5``` so keep that in mind too. Or not if you like errors.
|
51
|
+
```ruby
|
52
|
+
js_obj[5] = "hello"
|
53
|
+
js_obj[5] # => "hello"
|
54
|
+
js_obj["5"] # => "hello"
|
55
|
+
js_obj[:"5"] # => "hello"
|
56
|
+
js_obj.send(:"5") # => "hello"
|
57
|
+
```
|
58
|
+
Be careful with anything that isn't a string, symbol, number or something that doesn't implement ```#to_s``` like
|
59
|
+
Objects do (ie, like ```"#<Object:0x007f8003825200>"```). If changing the object changes the return value of ```#to_s```
|
60
|
+
then your JsObject will treat it like a different key. A couple examples are below:
|
61
|
+
```ruby
|
62
|
+
hash = { word: "hello", number: 5 }
|
63
|
+
hash.to_s # => "{:word=>\"hello\", :number=>5}"
|
64
|
+
js_obj[hash] = 5
|
65
|
+
js_obj[hash] # => 5
|
66
|
+
|
67
|
+
hash["new"] = "something"
|
68
|
+
hash.to_s # => "{:word=>\"hello\", :number=>5, \"new\"=>\"something\"}"
|
69
|
+
js_obj[hash] # => nil
|
70
|
+
|
71
|
+
|
72
|
+
array = [1, "two", 3]
|
73
|
+
array.to_s # => "[1, \"two\", 3]"
|
74
|
+
js_obj[array] = 8
|
75
|
+
js_obj[array] # => 8
|
76
|
+
|
77
|
+
array << 4
|
78
|
+
array.to_s # => "[1, \"two\", 3, 4]"
|
79
|
+
js_obj[array] # => nil
|
80
|
+
```
|
81
|
+
Of course, anything that doesn't implement ```#to_s``` will raise an error if you try to use it as a key:
|
82
|
+
```ruby
|
83
|
+
basic = BasicObject.new
|
84
|
+
js_obj[basic] # => NoMethodError: undefined method `to_s' for #<BasicObject:0x007f8001a1c6a0>
|
85
|
+
js_obj[basic] = 5 # => NoMethodError: undefined method `to_s' for #<BasicObject:0x007f8001a1c6a0>
|
86
|
+
js_obj[basic] # => NoMethodError: undefined method `to_s' for #<BasicObject:0x007f8001a1c6a0> still.
|
87
|
+
```
|
88
|
+
### Blocks, Procs, and Lambdas become methods.
|
89
|
+
If you set a Proc as a property, it becomes a method, but it's still accessible via ```#[]```.
|
90
|
+
```ruby
|
91
|
+
some_proc = Proc.new{ |x| x * x }
|
92
|
+
js_obj[:proc] = some_proc
|
93
|
+
js_obj["proc"] # => some_proc
|
94
|
+
js_obj.proc(3) # => 9
|
95
|
+
```
|
96
|
+
You can even set Procs that take Procs as methods:
|
97
|
+
```ruby
|
98
|
+
some_lambda = ->(x, &block){ block.call x }
|
99
|
+
js_obj.lambda = some_lambda
|
100
|
+
js_obj[:lambda] # => some_lambda
|
101
|
+
js_obj.lambda(2) { |z| z + 1 } # => 3
|
102
|
+
js_obj.lambda(2, &some_proc) # => 4
|
103
|
+
|
104
|
+
js_obj.new_method(&some_lambda)
|
105
|
+
js_obj.lambda(2) { |z| z + 1 } # => 3
|
106
|
+
|
107
|
+
js_obj.even_newer_method { |x| x.to_sym }
|
108
|
+
js_obj.even_newer_method("hi") # => :hi
|
109
|
+
|
110
|
+
js_obj.num = 5
|
111
|
+
js_obj.other_num = 3
|
112
|
+
```
|
113
|
+
Hate typing ```Proc.new``` or```->```? Well, you don't have to: You can simply call an unknown
|
114
|
+
method and pass it a block to define the method on the object. You can still access the Proc object
|
115
|
+
(say if you wanted another JS object to have the same method) via ```#[]```.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
js_obj.new_method { |x| x * x }
|
119
|
+
js_obj.new_method(5) # => 25
|
120
|
+
|
121
|
+
js_obj.another_method do |word|
|
122
|
+
word.upcase
|
123
|
+
end
|
124
|
+
js_obj.another_method("word") # => "WORD"
|
125
|
+
```
|
126
|
+
Also, every method set ona JS object is executed in the context of the object, so feel free to use ```self``` when defining methods
|
127
|
+
through blocks, procs, or lambdas. Just remember that you shouldn't use ```self``` in a block, proc, or lambda
|
128
|
+
that's passed as an argument to another method, unless you want that ```self``` to be the scope the proc was created
|
129
|
+
in. So this is good:
|
130
|
+
```ruby
|
131
|
+
js_obj.context_is? { self }
|
132
|
+
js_obj.context_is? # => js_obj
|
133
|
+
|
134
|
+
js_obj.number_method do |x|
|
135
|
+
self.num = x + self.num
|
136
|
+
self.num + self.other_num
|
137
|
+
end
|
138
|
+
|
139
|
+
js_obj.number_method(2) # => 10
|
140
|
+
js_obj.num # => 7
|
141
|
+
```
|
142
|
+
But this will be weird.
|
143
|
+
```ruby
|
144
|
+
js_obj.proc = Proc.new{ |&block| block.call }
|
145
|
+
js_obj.proc{ "this context: #{self}"" } # => "this context: main"
|
146
|
+
```
|
147
|
+
Sick of property or method on your object? Get rid of it with ```#delete```.
|
148
|
+
```ruby
|
149
|
+
js_obj.tired = "of this property"
|
150
|
+
js_obj.delete(:tired)
|
151
|
+
js_obj.tired # => nil
|
152
|
+
```
|
153
|
+
## Prototype
|
154
|
+
Prototype objects share a lot in common with JsObject objects as JsObject inherits from Prototype,
|
155
|
+
which inherits from Hash. The main difference between JsObject and Prototype does not implement
|
156
|
+
prototypal inheritance which sounds like it doesn't make sense, but don't worry: It does.
|
157
|
+
Unknown methods are not deferred to a prototype, they simply return ```nil```, unless you use the
|
158
|
+
Hash method ```#default=``` or ```#default_proc``` to specify a different return value. You can make
|
159
|
+
it raise an error so you don't get immediate errors instead of ```NoMethodError``` for ```nil``` later.
|
160
|
+
|
161
|
+
Since you have access to the Prototype class, you can utilize them as a kind of OpenStruct that can call
|
162
|
+
methods or have as many different prototypal inheritance trees as you'd like.
|
163
|
+
|
164
|
+
```PROTOTYPE``` is the default prototype for JsObjects. So instead of doing this:
|
165
|
+
```ruby
|
166
|
+
js_obj.something # => nil
|
167
|
+
js_obj.prototype.something = "something"
|
168
|
+
js_obj.something # => "something"
|
169
|
+
```
|
170
|
+
You can just do this:
|
171
|
+
```ruby
|
172
|
+
js_obj.something # => nil
|
173
|
+
PROTOTYPE.something = "something"
|
174
|
+
js_obj.something # => "something"
|
175
|
+
```
|
176
|
+
Whatever you want.
|
177
|
+
## Prototypal Inheritance
|
178
|
+
[Protoypal Inheritance](http://en.wikipedia.org/wiki/Prototype-based_programming) can be read about that link. The short version is that objects don't inherit from a class, but instead inherit methods and attributes from a prototype.
|
179
|
+
|
180
|
+
For instance, let's say we have 3 objects: ```PROTOTYPE``` (the default instance of the Prototype class and the default prototype
|
181
|
+
for JsObjects), ```js_obj```, and ```js_obj2```.
|
182
|
+
```ruby
|
183
|
+
js_obj2.prototype = js_obj
|
184
|
+
|
185
|
+
PROTOTYPE == js_obj.prototype # => true
|
186
|
+
js_obj2.prototype == js_obj # => true
|
187
|
+
js_obj2.prototype.prototype == PROTOTYPE # => true
|
188
|
+
```
|
189
|
+
If a method or attribute is defined somewhere in an object's inheritance tree, then that value will bubble up.
|
190
|
+
```ruby
|
191
|
+
PROTOTYPE.something = "something"
|
192
|
+
|
193
|
+
PROTOTYPE.something # => "something"
|
194
|
+
js_obj.something # => "something"
|
195
|
+
js_obj2.something # => "something"
|
196
|
+
|
197
|
+
js_obj.something_else = "something else"
|
198
|
+
PROTOTYPE.something_else # => nil
|
199
|
+
js_obj.something_else # => "something else"
|
200
|
+
js_obj2.something_else # => "something else"
|
201
|
+
|
202
|
+
js_obj2.another_thing = "anothing thing"
|
203
|
+
|
204
|
+
PROTOTYPE.another_thing # => nil
|
205
|
+
js_obj.another_thing # => nil
|
206
|
+
js_obj2.another_thing # => "another thing"
|
207
|
+
```
|
208
|
+
This also applies to procs, blocks, and lambdas. If you have a proc set on an object's prototype,
|
209
|
+
then calling it on the child object will invoke in the context of the child object. So:
|
210
|
+
```ruby
|
211
|
+
PROTOTYPE.set_something { self.something = "something" }
|
212
|
+
PROTOTYPE.something # => nil
|
213
|
+
js_obj.something # => nil
|
214
|
+
|
215
|
+
js_obj.set_something
|
216
|
+
js_obj.something # => "something"
|
217
|
+
PROTOTYPE.something # => nil
|
218
|
+
```
|
219
|
+
If you don't want to invoke the proc in the context of the object and would rather get
|
220
|
+
at the proc object itself, then just use ```#[]```.
|
221
|
+
```ruby
|
222
|
+
PROTOTYPE.proc { |x| x * 2 }
|
223
|
+
|
224
|
+
js_obj[:proc] # => (a Proc-ified version of the block used to def the proc method above)
|
225
|
+
```
|
226
|
+
## Frequently Asked Questions (aka FAQ)
|
227
|
+
### Why would anyone want this?
|
228
|
+
* Why do you gotta hate?
|
229
|
+
* If they really liked Javascript objects but had to write Ruby.
|
230
|
+
* If they hate brackets and would rather just call methods on their objects
|
231
|
+
* If they hate classical inheritance and prefer prototypal inheritance.
|
232
|
+
* If they wanted really badass OpenStructs with prototypal inheritance.
|
233
|
+
|
234
|
+
### Why not make this out of OpenStruct instead of Hash?
|
235
|
+
Then it wouldn't work in Ruby 1.9.3. OpenStructs only got the cool new ```#[]``` syntax in Ruby 2.0.
|
236
|
+
Also, OpenStructs are pretty bare methodwise while Hashes already have a ton of methods.
|
237
|
+
That is, I'd rather wrap those methods with super than write a bunch new methods for OpenStruct.
|
238
|
+
Granted, I haven't actually wrapped a lot of those methods yet, but I probably will at some point.
|
239
|
+
### Why not make this out of HashWithIndifferentAccess?
|
240
|
+
It was pretty easy to make it pretty indifferent without pulling in _all_ of ActiveSupport, so I did
|
241
|
+
that instead. Plus, in Javascript you can't really use Arrays and Objects as keys, so partially taking
|
242
|
+
that away actually makes this more like Javascript. There are always tradeoffs and in this case not using
|
243
|
+
ActiveSupport costs us being able to use some objects as keys. That's fine with me.
|
244
|
+
### Everyone hates Javascript and prefers Ruby, so why make Ruby more like Javascript?
|
245
|
+
Again, why do you gotta hate? In particular, on Javascript? I understand what you mean, but
|
246
|
+
this was a good learning exercise. Besides, prototypal inheritance and passing procs around can actually
|
247
|
+
create some pretty expressive and powerful code. Maybe this will help you practice Javascript and you'll
|
248
|
+
eventually learn to love it.
|
249
|
+
|
250
|
+
|
251
|
+
|
data/lib/js_object.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
PROTOTYPE = Prototype.new
|
2
|
+
|
3
|
+
class JsObject < Prototype
|
4
|
+
|
5
|
+
def initialize(prototype=nil)
|
6
|
+
self.nil_keys = []
|
7
|
+
self.false_keys = []
|
8
|
+
self[:prototype] = prototype || PROTOTYPE
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
if nil_keys.include? key.to_s
|
13
|
+
nil
|
14
|
+
elsif false_keys.include? key.to_s
|
15
|
+
false
|
16
|
+
else
|
17
|
+
super || prototype[key]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(key, value)
|
22
|
+
remove_from_falsey_lists key.to_s
|
23
|
+
add_to_falsey_lists key.to_s, value
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(property)
|
28
|
+
remove_from_falsey_lists property.to_s
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_accessor :nil_keys, :false_keys
|
35
|
+
|
36
|
+
def remove_from_falsey_lists(key)
|
37
|
+
nil_keys.delete key
|
38
|
+
false_keys.delete key
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_to_falsey_lists(key, value)
|
42
|
+
if value.nil?
|
43
|
+
nil_keys << key
|
44
|
+
elsif value == false
|
45
|
+
false_keys << key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(method, *arguments, &block)
|
50
|
+
return super if equals_method?(method) || (block && prototype[method].nil?)
|
51
|
+
delegate_to_prototype method, arguments, block
|
52
|
+
end
|
53
|
+
|
54
|
+
def delegate_to_prototype(method_name, arguments, block)
|
55
|
+
prototypes_value = prototype[method_name]
|
56
|
+
if prototypes_value.kind_of? Proc
|
57
|
+
define_singleton_method :__proto_proc, &prototypes_value
|
58
|
+
__proto_proc *arguments, &block
|
59
|
+
else
|
60
|
+
prototypes_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/js_objects.rb
ADDED
data/lib/prototype.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
class Prototype < Hash
|
2
|
+
|
3
|
+
def [](key)
|
4
|
+
super(key.to_s)
|
5
|
+
end
|
6
|
+
|
7
|
+
def []=(key, value)
|
8
|
+
define_methods(key.to_s, value)
|
9
|
+
super(key.to_s, value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete(property)
|
13
|
+
singleton_class.send :remove_method, property
|
14
|
+
singleton_class.send :remove_method, getter_to_setter_name(property)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def method_missing(method, *arguments, &block)
|
21
|
+
if equals_method?(method)
|
22
|
+
self[setter_to_getter_name(method)] = arguments.first
|
23
|
+
elsif block
|
24
|
+
self[method] = block
|
25
|
+
else
|
26
|
+
self[method]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def setter_to_getter_name(setter_name)
|
31
|
+
setter_name.to_s.chop.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def getter_to_setter_name(getter_name)
|
35
|
+
"#{getter_name}=".to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
def equals_method?(method_name)
|
39
|
+
method_name.to_s[-1] == '=' && method_name.to_s[-2] != '='
|
40
|
+
end
|
41
|
+
|
42
|
+
def define_setter_method(method_name)
|
43
|
+
define_singleton_method method_name do |new_value|
|
44
|
+
self[setter_to_getter_name(method_name)] = new_value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def define_proc_getter_method(method_name, proc)
|
49
|
+
define_singleton_method method_name, &proc
|
50
|
+
end
|
51
|
+
|
52
|
+
def define_getter_method(method_name)
|
53
|
+
define_singleton_method method_name do
|
54
|
+
self[method_name]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def define_methods(method_name, value)
|
59
|
+
define_setter_method getter_to_setter_name(method_name) unless respond_to? method_name
|
60
|
+
if value.kind_of? Proc
|
61
|
+
define_proc_getter_method method_name, value
|
62
|
+
else
|
63
|
+
define_getter_method method_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: js_objects
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.6.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Crismali
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-17 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Provides objects with some JavaScript-like syntax and prototypal inheritance
|
15
|
+
in Ruby
|
16
|
+
email: michael.crismali@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- lib/js_object.rb
|
23
|
+
- lib/js_objects.rb
|
24
|
+
- lib/prototype.rb
|
25
|
+
homepage: https://github.com/michaelcrismali/js_objects_gem
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 1.8.23
|
47
|
+
signing_key:
|
48
|
+
specification_version: 3
|
49
|
+
summary: Provides objects with some JavaScript-like syntax and prototypal inheritance
|
50
|
+
in Ruby
|
51
|
+
test_files: []
|