zanzou 0.1.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.
@@ -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: []