zanzou 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 562e3759ee6493bda4a1f63cff36cbe0d936e53f47317c5ab8dceda53b5a62be
4
+ data.tar.gz: e5167c410c4d0c3d7e1a4db3a017d8d43a4b9bcbff92a9dbd8b7b37776d0c9d2
5
+ SHA512:
6
+ metadata.gz: 3cb4b2d79d5e2b790f061457b27e978801aa507297542ff8aba2f1c76413055e01814f1315693c619b991c2376c92fce1aa06c419483d42e8e1df984b248a691
7
+ data.tar.gz: a3fdf7818710d1e178894cdc1d90d790cd602bce9c40d549aabdbc3bf9837b48c35a8bf96bf1eaecca2c2e9a36db3b54e308e79f0251c81ac4d70c8785330a10
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,3 @@
1
+ # v0.1.0 (2019/06/29)
2
+
3
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rake'
4
+ gem 'rspec'
5
+ gem 'simplecov'
@@ -0,0 +1,36 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.3)
5
+ docile (1.3.2)
6
+ json (2.2.0)
7
+ rake (10.5.0)
8
+ rspec (3.8.0)
9
+ rspec-core (~> 3.8.0)
10
+ rspec-expectations (~> 3.8.0)
11
+ rspec-mocks (~> 3.8.0)
12
+ rspec-core (3.8.1)
13
+ rspec-support (~> 3.8.0)
14
+ rspec-expectations (3.8.4)
15
+ diff-lcs (>= 1.2.0, < 2.0)
16
+ rspec-support (~> 3.8.0)
17
+ rspec-mocks (3.8.1)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.8.0)
20
+ rspec-support (3.8.2)
21
+ simplecov (0.16.1)
22
+ docile (~> 1.1)
23
+ json (>= 1.8, < 3)
24
+ simplecov-html (~> 0.10.0)
25
+ simplecov-html (0.10.2)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ rake
32
+ rspec
33
+ simplecov
34
+
35
+ BUNDLED WITH
36
+ 2.0.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Yutaka HARA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,105 @@
1
+ # Zanzou
2
+
3
+ Something like Ruby port of [immer.js](https://github.com/immerjs/immer)
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'zanzou'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install zanzou
20
+
21
+ ## Usage
22
+
23
+ With `Zanzou.with_updates`, you can create a modified copy of `orig_obj` with
24
+ imperative style.
25
+
26
+ ```rb
27
+ require 'zanzou'
28
+ orig_obj = {a: 1, b: 2}
29
+ p Zanzou.with_updates(orig_obj){|o| o[:b] = 3}
30
+ # => {a: 1, b: 3}
31
+ p orig_obj
32
+ # => {a: 1, b: 2}
33
+ ```
34
+
35
+ Or you can just call `.with_updates` by requir'ing `zanzou/install`.
36
+
37
+ ```rb
38
+ require 'zanzou/install'
39
+ orig_obj = {a: 1, b: 2}
40
+ p orig_obj.with_updates{|o| o[:b] = 3}
41
+ p orig_obj
42
+ ```
43
+
44
+ ## Supported classes
45
+
46
+ Zanzou should work well with these objects.
47
+
48
+ - Array, Hash, String, numbers, true, false, nil
49
+
50
+ Normally other objects, say a Range, should be OK too. However, container
51
+ classes (i.e. objects which contains other objects in it) need special
52
+ treatment to be used with Zanzou.
53
+
54
+ Steps to add support for a container class:
55
+
56
+ 1. Define `class XxxShadow < Zanzou::ShadowNode`
57
+
58
+ See spec/zanzou_spec.rb for an example.
59
+
60
+ ## Known issues
61
+
62
+ Some methods of Array/Hash may does not work well. See the pending specs (`git grep pending`).
63
+
64
+ FYI immer.js does not have such problems because ES6 Proxy is very powerful - it
65
+ reports object set/get even for methods like Array sort.
66
+
67
+ ```js
68
+ const hooks = {
69
+ get(target, prop, receiver) {
70
+ console.log({hook: "get", prop})
71
+ return target[prop];
72
+ },
73
+ set(target, prop, value) {
74
+ console.log({hook: "set", prop, value})
75
+ return target[prop] = value;
76
+ }
77
+ };
78
+
79
+ const obj = [5613,2348,2987,2387,7823,1987];
80
+ const pxy = new Proxy(obj, hooks);
81
+ pxy.sort();
82
+
83
+ // Output:
84
+ // ...
85
+ // { hook: 'get', prop: '4' }
86
+ // { hook: 'get', prop: '5' }
87
+ // { hook: 'set', prop: '0', value: 1987 } // It reports all the set/get operations
88
+ // { hook: 'set', prop: '1', value: 2348 } // sort() does and thus immer.js can
89
+ // { hook: 'set', prop: '2', value: 2387 } // track movements of child elements.
90
+ // { hook: 'set', prop: '3', value: 2987 }
91
+ // { hook: 'set', prop: '4', value: 5613 }
92
+ // { hook: 'set', prop: '5', value: 7823 }
93
+ ```
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yhara/zanzou.
98
+
99
+ ### How to run test
100
+
101
+ bundle exec rspec
102
+
103
+ ## License
104
+
105
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,17 @@
1
+ #require "bundler/gem_tasks"
2
+ task :default => :spec
3
+
4
+ desc "git ci, git tag and git push"
5
+ task :release do
6
+ load 'lib/zanzou/version.rb'
7
+ sh "git diff HEAD"
8
+ v = "v#{Zanzou::VERSION}"
9
+ puts "release as #{v}? [y/N]"
10
+ break unless $stdin.gets.chomp == "y"
11
+
12
+ sh "gem build zanzou" # First, make sure we can build gem
13
+ sh "git ci -am '#{v}'"
14
+ sh "git tag '#{v}'"
15
+ sh "git push origin master --tags"
16
+ sh "gem push zanzou-#{Zanzou::VERSION}.gem"
17
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "zanzou"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,206 @@
1
+ require "zanzou/version"
2
+
3
+ module Zanzou
4
+ class ShadowNode
5
+ IMMUTABLE_CLASSES = [
6
+ TrueClass, FalseClass, NilClass,
7
+ Symbol, Numeric
8
+ ]
9
+
10
+ def self.create(orig_obj, parent:, parent_key:)
11
+ case orig_obj
12
+ when Array
13
+ ArrayShadow.new(orig_obj, parent: parent, parent_key: parent_key)
14
+ when Hash
15
+ HashShadow.new(orig_obj, parent: parent, parent_key: parent_key)
16
+ else
17
+ if orig_obj.frozen? || IMMUTABLE_CLASSES.include?(orig_obj.class)
18
+ orig_obj
19
+ elsif orig_obj.respond_to?(:zanzou_class)
20
+ orig_obj.zanzou_class.new(orig_obj, parent: parent, parent_key: parent_key)
21
+ else
22
+ AnyObjectShadow.new(orig_obj, parent: parent, parent_key: parent_key)
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.finalize(shadow)
28
+ orig_obj = shadow.instance_variable_get(:@orig_obj)
29
+ modified = shadow.instance_variable_get(:@modified)
30
+ new_obj = shadow.instance_variable_get(:@new_obj)
31
+ modifications = shadow.instance_variable_get(:@modifications)
32
+ modifications.transform_values!{|v|
33
+ ShadowNode === v ? ShadowNode.finalize(v) : v
34
+ }
35
+
36
+ #pp cls: shadow.class.name, orig_obj: orig_obj, modified: modified, modifications: modifications, new_obj: new_obj
37
+ if modified
38
+ if new_obj
39
+ if modifications.empty?
40
+ ret = new_obj
41
+ else
42
+ ret = shadow.class.merge(new_obj, modifications)
43
+ end
44
+ else
45
+ if modifications.empty?
46
+ ret = orig_obj
47
+ else
48
+ ret = shadow.class.merge(orig_obj, modifications)
49
+ end
50
+ end
51
+ else
52
+ ret = orig_obj
53
+ end
54
+ #pp ret: ret
55
+ return ret
56
+ end
57
+
58
+ def initialize(orig_obj, parent:, parent_key:)
59
+ @orig_obj, @parent, @parent_key = orig_obj, parent, parent_key
60
+ @modified = false
61
+ @modifications = {}
62
+ @new_obj = nil
63
+ end
64
+
65
+ private
66
+
67
+ def handle_destructive_method_call(name, args)
68
+ modified!
69
+ @new_obj = @orig_obj.dup
70
+ return @new_obj.public_send(name, *args)
71
+ end
72
+
73
+ def handle_non_destructive_method_call(name, args)
74
+ return (@new_obj || @orig_obj).public_send(name, *args)
75
+ end
76
+
77
+ def modified!
78
+ @modified = true
79
+
80
+ # Mark ancestors to be modified
81
+ parent = @parent
82
+ while parent && !parent.instance_variable_get(:@modified)
83
+ parent.instance_variable_set(:@modified, true)
84
+ parent = parent.instance_variable_get(:@parent)
85
+ end
86
+
87
+ # Tell parent for the modification
88
+ if @parent
89
+ @parent.instance_variable_get(:@modifications)[@parent_key] = self
90
+ end
91
+ end
92
+ end
93
+
94
+ class HashShadow < ShadowNode
95
+ def self.merge(orig_hash, modifications)
96
+ orig_hash.merge(modifications)
97
+ end
98
+
99
+ def method_missing(name, *args)
100
+ case name
101
+ when :[]=
102
+ handle_setter(args[0], args[1])
103
+ when :[]
104
+ handle_getter(args[0])
105
+ else
106
+ handle_destructive_method_call(name, args)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def handle_setter(key, value)
113
+ modified!
114
+ @modifications[key] = value
115
+ return value
116
+ end
117
+
118
+ def handle_getter(key)
119
+ if @modifications.key?(key)
120
+ return @modifications[key]
121
+ else
122
+ return ShadowNode.create(@orig_obj.public_send(key), parent: self, parent_key: key)
123
+ end
124
+ end
125
+ end
126
+
127
+ class ArrayShadow < ShadowNode
128
+ def self.merge(orig_ary, modifications)
129
+ ret = orig_ary.dup
130
+ modifications.each{|k, v| ret[k] = v}
131
+ ret
132
+ end
133
+
134
+ def initialize(*args)
135
+ super
136
+ @children = {}
137
+ end
138
+
139
+ def method_missing(name, *args)
140
+ case name
141
+ when :[]=
142
+ handle_setter(args[0], args[1])
143
+ when :[]
144
+ handle_getter(args[0])
145
+ else
146
+ handle_destructive_method_call(name, args)
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def handle_setter(key, value)
153
+ modified!
154
+ @modifications[key] = value
155
+ return value
156
+ end
157
+
158
+ def handle_getter(idx)
159
+ if @new_obj
160
+ return @new_obj[idx]
161
+ else
162
+ if @children.key?(idx)
163
+ return @children[key]
164
+ else
165
+ child_shadow = ShadowNode.create(@orig_obj[idx], parent: self, parent_key: idx)
166
+ @children[idx] = child_shadow
167
+ return child_shadow
168
+ end
169
+ end
170
+ end
171
+
172
+ def handle_destructive_method_call(name, args)
173
+ modified!
174
+ @new_obj = @orig_obj.dup
175
+
176
+ # Apply modification to children now because the index may change by the public_send
177
+ @children.each do |idx, child_shadow|
178
+ @new_obj[idx] = ShadowNode.finalize(child_shadow)
179
+ end
180
+ # Forget about the children we've finalized
181
+ @modifications.clear
182
+
183
+ return @new_obj.public_send(name, *args)
184
+ end
185
+ end
186
+
187
+ # Shadow for any Ruby objects (except container objects, which needs
188
+ # special Shadow class to handle parent-child relationship).
189
+ # We know nothing about the class, so assume all methods are
190
+ # destructive (pessimistic)
191
+ class AnyObjectShadow < ShadowNode
192
+ def method_missing(name, *args)
193
+ return handle_destructive_method_call(name, args)
194
+ end
195
+ end
196
+
197
+ def with_updates(&block)
198
+ Zanzou.with_updates(self, &block)
199
+ end
200
+
201
+ def self.with_updates(obj, &block)
202
+ shadow = ShadowNode.create(obj, parent: nil, parent_key: nil)
203
+ block.call(shadow)
204
+ return ShadowNode.finalize(shadow)
205
+ end
206
+ end
@@ -0,0 +1,31 @@
1
+ # require'ing this file improves Zanzou's performance, but it will
2
+ # break if any of these methods are redefined to be destructive.
3
+ require 'zanzou/whitelist'
4
+
5
+ module Zanzou
6
+ module WhitelistArrayMethods
7
+ NON_DESTRUCTIVE_ARRAY_METHODS = %i(
8
+ & * + - <=> == [] at assoc bsearch bsearch_index clone dup
9
+ combination compact cycle difference dig each each_index empty?
10
+ eql? fetch find_index index first flatten hash include? inspect
11
+ to_s join last length size max min pack permutation pop product
12
+ rassoc repeated_combination repeated_permutation reverse
13
+ reverse_each rindex rotate sample shuffle slice sort sum
14
+ to_a to_ary to_h transpose union uniq values_at zip |
15
+ ) +
16
+ NON_DESTRUCTIVE_BASIC_OBJECT_METHODS +
17
+ NON_DESTRUCTIVE_OBJECT_METHODS
18
+
19
+ def method_missing(name, *args)
20
+ if NON_DESTRUCTIVE_ARRAY_METHODS.include?(name)
21
+ handle_non_destructive_method_call(name, args)
22
+ else
23
+ super
24
+ end
25
+ end
26
+ end
27
+
28
+ class ArrayShadow < ShadowNode
29
+ prepend WhitelistArrayMethods
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # require'ing this file improves Zanzou's performance, but it will
2
+ # break if any of these methods are redefined to be destructive.
3
+ require 'zanzou/whitelist'
4
+
5
+ module Zanzou
6
+ module WhitelistHashMethods
7
+ NON_DESTRUCTIVE_HASH_METHODS = %i(
8
+ < <= == === eql? > >= [] assoc clone dup compact compare_by_identity?
9
+ default default_proc dig each each_pair each_key each_value empty?
10
+ equal? fetch fetch_values filter select flatten has_key? include?
11
+ key? member? has_value? value? hash index key inspect to_s invert
12
+ keys length size merge rassoc reject slice sort to_a to_h to_hash
13
+ to_proc transform_keys transform_values values values_at
14
+ ) +
15
+ NON_DESTRUCTIVE_BASIC_OBJECT_METHODS +
16
+ NON_DESTRUCTIVE_OBJECT_METHODS
17
+
18
+ def method_missing(name, *args)
19
+ if NON_DESTRUCTIVE_METHODS.include?(name)
20
+ handle_non_destructive_method_call(name, args)
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+ class HashShadow < ShadowNode
28
+ prepend WhitelistHashMethods
29
+ end
30
+ end
31
+
@@ -0,0 +1,7 @@
1
+ require 'zanzou'
2
+
3
+ class Object
4
+ def with_updates(&block)
5
+ Zanzou.with_updates(self, &block)
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Zanzou
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,16 @@
1
+ module Zanzou
2
+ NON_DESTRUCTIVE_BASIC_OBJECT_METHODS = %i(
3
+ ! != == __id__ equal?
4
+ )
5
+
6
+ NON_DESTRUCTIVE_OBJECT_METHODS = %i(
7
+ ~ <=> == === =~ class clone dup display enum_for to_enum eql? equal?
8
+ frozen? hash inspect instance_of? instance_variable_defined?
9
+ instance_variable_get instance_variables is_a? kind_of? itself
10
+ method methods nil? object_id private_methods protected_methods
11
+ public_method public_methods respond_to? singleton_class
12
+ singleton_method singleton_methods tainted? tap then yield_self
13
+ to_a to_ary to_hash to_int to_io to_proc to_regexp to_s to_str
14
+ untrusted?
15
+ )
16
+ end
@@ -0,0 +1,27 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "zanzou/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "zanzou"
8
+ spec.version = Zanzou::VERSION
9
+ spec.authors = ["Yutaka HARA"]
10
+ spec.email = ["yutaka.hara+github@gmail.com"]
11
+
12
+ spec.summary = %q{Provides DSL for "modifying immutable objects"}
13
+ spec.homepage = "https://github.com/yhara/zanzou"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zanzou
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yutaka HARA
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-06-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - yutaka.hara+github@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - bin/console
28
+ - bin/setup
29
+ - lib/zanzou.rb
30
+ - lib/zanzou/array.rb
31
+ - lib/zanzou/hash.rb
32
+ - lib/zanzou/install.rb
33
+ - lib/zanzou/version.rb
34
+ - lib/zanzou/whitelist.rb
35
+ - zanzou.gemspec
36
+ homepage: https://github.com/yhara/zanzou
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ homepage_uri: https://github.com/yhara/zanzou
41
+ source_code_uri: https://github.com/yhara/zanzou
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.0.3
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Provides DSL for "modifying immutable objects"
61
+ test_files: []