leto 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 539f52438c18bb93115734b5c3c9dd5f8678886d392357c4fdca658b43d95357
4
+ data.tar.gz: bbe092a276d3241d39857cb81e8fb748108b15e67cbc0e421b7390545b8a066a
5
+ SHA512:
6
+ metadata.gz: 9a0e610d258c35cf044c0118ccc10dff918edc8495df7448c058dbe6ad3201e540952fa67ac14e065229e1dd961788936e1343d30254743f47319609b5b4e169
7
+ data.tar.gz: c7e0fa4e9be9c2c0da092a7dbb88c78b9b11a12d8fb4f9d34e2ed97794ab2f26282c1fe31b04db5c70039198e675d2770ab0a7ae52edcb28049e7568c6c7bc56
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ inherit_gem:
2
+ relaxed-rubocop: .rubocop.yml
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+ SuggestExtensions: false
7
+ TargetRubyVersion: 2.6
8
+
9
+ Gemspec/RequiredRubyVersion:
10
+ Enabled: false
11
+
12
+ Gemspec/RequireMFA:
13
+ Enabled: false
14
+
15
+ Layout/LineLength:
16
+ Max: 80
17
+
18
+ Lint/AmbiguousBlockAssociation:
19
+ Enabled: false
20
+
21
+ Lint/AmbiguousOperatorPrecedence:
22
+ Enabled: false
23
+
24
+ Style/FetchEnvVar:
25
+ Enabled: false
26
+
27
+ Style/FrozenStringLiteralComment:
28
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2023-02-24
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in leto.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "rubocop", "~> 1.21"
13
+
14
+ gem "relaxed-rubocop"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Janosch Müller
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.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Leto
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/leto.svg)](http://badge.fury.io/rb/leto)
4
+ [![Build Status](https://github.com/jaynetics/leto/workflows/tests/badge.svg)](https://github.com/jaynetics/leto/actions)
5
+
6
+ A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
7
+
8
+ Takes an object and recursively yields:
9
+
10
+ - the given object
11
+ - instance variables, class variables, constants
12
+ - Hash keys and values
13
+ - Enumerable members
14
+ - Struct members
15
+ - Range begins and ends
16
+
17
+ This makes stuff like deep-freezing fairly easy to implement:
18
+
19
+ ```ruby
20
+ my_object = [ { a: ['b', 'c'..'d'] } ]
21
+
22
+ Leto.call(my_object, &:freeze)
23
+
24
+ my_object.frozen? # => true
25
+ my_object[0].frozen? # => true
26
+ my_object[0][:a][1].begin.frozen? # => true
27
+ ```
28
+
29
+ Note: the slightly smarter `Leto.deep_freeze` is one of the [included utility methods](#included-utility-methods).
30
+
31
+ ## Usage
32
+
33
+ Basic example:
34
+
35
+ ```ruby
36
+ object = [{ a: ['b', 'c'..'d'] }]
37
+
38
+ Leto.call(object) { |el| p el }
39
+ # prints:
40
+ #
41
+ # [{:a=>["b", ["c".."d"]]}]
42
+ # {:a=>["b", ["c".."d"]]}
43
+ # :a
44
+ # ["b", ["c".."d"]]
45
+ # "b"
46
+ # "c".."d"
47
+ # "c"
48
+ # "d"
49
+
50
+ Leto.call(object).to_a
51
+ # => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
52
+
53
+ # all (sub-)objects have a path:
54
+ Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
+ # prints:
56
+ #
57
+ # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
58
+ # {:a=>["b", "c".."d"]} @#<Leto::Path [{:a=>["b", "c".."d"]}][0]>
59
+ # :a @#<Leto::Path [{:a=>["b", "c".."d"]}][0].keys[0]>
60
+ # ["b", "c".."d"] @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a]>
61
+ # "b" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][0]>
62
+ # "c".."d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1]>
63
+ # "c" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].begin>
64
+ # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
65
+
66
+ # paths can be looked up with Leto::Path#resolve or Leto::dig
67
+ path = Leto.call(object).map { |_el, path| path }.last # => #<Leto::Path...>
68
+ path.resolve # => "d"
69
+ Leto.dig(object, path) # => "d"
70
+ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
71
+ ```
72
+
73
+ ### Included utility methods
74
+
75
+ - `Leto.deep_freeze(obj)`
76
+ - similar to the version above, but avoids freezing Modules and unfreezables
77
+ - `Leto.deep_print(obj)`
78
+ - for debugging - prints more information than `pretty_print` does by default
79
+ - `Leto.deep_eql?(obj1, obj2)`
80
+ - stricter version of `#eql?` that takes all ivars into consideration
81
+ - `Leto.shared_mutable_state?(obj1, obj2)`
82
+ - useful for debugging or verifying that a `#dup` implementation is sane
83
+ - `Leto.shared_mutables(obj1, obj2)`
84
+ - useful for debugging or verifying that a `#dup` implementation is sane
85
+ - `Leto.shared_objects(obj1, obj2)`
86
+ - returns all objects shared by `obj1` and `obj2`, whether mutable or not
87
+
88
+ ## Benchmarks
89
+
90
+ ```
91
+ Leto.deep_freeze: 8762.1 i/s
92
+ IceNine.deep_freeze: 7390.3 i/s - 1.19x (± 0.00) slower
93
+ ```
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jaynetics/leto.
98
+
99
+ ## License
100
+
101
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/benchmark.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'benchmark/ips'
2
+ require 'ice_nine'
3
+ require_relative 'lib/leto'
4
+
5
+ obj = (0..9).to_h { |n| [n, [{ false => true, sym: ('a'..'z').to_a.shuffle }]] }
6
+
7
+ Benchmark.ips do |x|
8
+ x.report('Leto.deep_freeze') { Leto.deep_freeze(obj) }
9
+ x.report('IceNine.deep_freeze') { IceNine.deep_freeze(obj) }
10
+ x.compare!
11
+ end; nil
data/leto.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/leto/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "leto"
7
+ spec.version = Leto::VERSION
8
+ spec.authors = ["Janosch Müller"]
9
+ spec.email = ["janosch84@gmail.com"]
10
+
11
+ spec.summary = "Generic object traverser"
12
+ spec.homepage = "https://github.com/jaynetics/leto"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+
19
+ spec.files = Dir.chdir(__dir__) do
20
+ `git ls-files -z`.split("\x0").reject do |f|
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
22
+ end
23
+ end
24
+ spec.require_paths = ["lib"]
25
+ end
data/lib/leto/call.rb ADDED
@@ -0,0 +1,64 @@
1
+ module Leto
2
+ LEAKY_PROCESS_TMS = RUBY_VERSION.to_f <= 2.7
3
+
4
+ def self.call(obj, &block)
5
+ return enum_for(__method__, obj) unless block_given?
6
+
7
+ seen = {}.tap(&:compare_by_identity)
8
+ seen[Process::Tms] = true if LEAKY_PROCESS_TMS
9
+ path = block.arity == 2 ? Path.new(start: obj) : nil
10
+ traverse(obj, path, seen, block)
11
+ obj
12
+ end
13
+
14
+ def self.traverse(obj, path, seen, block)
15
+ return if seen[obj]
16
+
17
+ seen[obj] = true
18
+
19
+ path ? block.call(obj, path) : block.call(obj)
20
+
21
+ obj.instance_variables.each do |ivar_name|
22
+ traverse(
23
+ obj.instance_variable_get(ivar_name),
24
+ path&.+([[:instance_variable_get, ivar_name]]),
25
+ seen, block
26
+ )
27
+ end
28
+
29
+ case obj
30
+ when Hash
31
+ obj.keys.each_with_index do |k, i|
32
+ traverse(k, path&.+([[:keys], [:[], i]]), seen, block)
33
+ traverse(obj[k], path&.+([[:[], k]]), seen, block)
34
+ end
35
+ when Module
36
+ obj.class_variables.each do |cvar_name|
37
+ traverse(
38
+ obj.class_variable_get(cvar_name),
39
+ path&.+([[:class_variable_get, cvar_name]]),
40
+ seen, block
41
+ )
42
+ end
43
+ obj.constants.each do |const_name|
44
+ traverse(
45
+ obj.const_get(const_name),
46
+ path&.+([[:const_get, const_name]]),
47
+ seen, block
48
+ )
49
+ end
50
+ when Range
51
+ traverse(obj.begin, path&.+([[:begin]]), seen, block)
52
+ traverse(obj.end, path&.+([[:end]]), seen, block)
53
+ when Struct
54
+ obj.members.each do |member|
55
+ traverse(obj[member], path&.+([[:[], member]]), seen, block)
56
+ end
57
+ when Enumerable
58
+ obj.each_with_index do |el, idx|
59
+ traverse(el, path&.+([[:[], idx]]), seen, block)
60
+ end
61
+ end
62
+ end
63
+ private_class_method :traverse
64
+ end
data/lib/leto/dig.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Leto
2
+ def self.dig(obj, steps)
3
+ Leto::Path.new(start: obj, steps: steps).resolve
4
+ end
5
+ end
data/lib/leto/path.rb ADDED
@@ -0,0 +1,51 @@
1
+ # Light wrapper around Array, mostly for nicer display.
2
+ module Leto
3
+ class Path
4
+ include Enumerable
5
+
6
+ attr_reader :start, :steps
7
+
8
+ def initialize(start:, steps: [])
9
+ @start = start
10
+ @steps = steps
11
+ end
12
+
13
+ def each(&block)
14
+ steps.each(&block)
15
+ end
16
+
17
+ def +(other)
18
+ self.class.new(start: start, steps: steps + other.to_a)
19
+ end
20
+
21
+ def resolve
22
+ steps.inject(start) do |obj, (method, *args)|
23
+ obj&.send(method, *args) or break obj
24
+ rescue StandardError => e
25
+ warn "#{__method__}: #{e.class} #{e.message}"
26
+ end
27
+ end
28
+
29
+ def ==(other)
30
+ other.to_a == steps
31
+ end
32
+
33
+ def inspect
34
+ start_str = start.inspect
35
+ start_str = "#{start_str[0..38]}…#{start_str[-1]}" if start_str.size > 40
36
+ [
37
+ "#<#{self.class} #{start_str}",
38
+ steps.map do |method, *args|
39
+ args_str = args.map(&:inspect).join(', ')
40
+ if method == :[]
41
+ "[#{args_str}]"
42
+ else
43
+ ".#{method}#{"(#{args_str})" unless args_str.empty?}"
44
+ end
45
+ end,
46
+ ">"
47
+ ].join
48
+ end
49
+ alias to_s inspect
50
+ end
51
+ end
data/lib/leto/utils.rb ADDED
@@ -0,0 +1,64 @@
1
+ module Leto
2
+ def self.deep_freeze(obj, include_modules: false)
3
+ call(obj) do |el|
4
+ el.freeze if el.respond_to?(:freeze) &&
5
+ !el.frozen? &&
6
+ (include_modules || !el.is_a?(Module))
7
+ end
8
+ end
9
+
10
+ def self.deep_print(obj, print_method: :inspect, indent: 4, show_path: true)
11
+ call(obj) do |el, path|
12
+ puts "#{' ' * path.count * indent}#{el.send(print_method)}" \
13
+ "#{" @ #{path.inspect}" if show_path}" \
14
+ [0..78]
15
+ end
16
+ nil
17
+ end
18
+
19
+ def self.deep_eql?(obj1, obj2)
20
+ call(obj1).to_a == call(obj2).to_a
21
+ end
22
+
23
+ def self.shared_mutable_state?(obj1, obj2)
24
+ shared_mutables(obj1, obj2).any?
25
+ end
26
+
27
+ # returns [[shared_object, path1, path2], ...], e.g.:
28
+ # str1 = 'foo'.dup
29
+ # str2 = 'bar'.dup
30
+ # shared_mutables([str1, str2], [str2, str1]) # =>
31
+ # [
32
+ # ["foo", [[:[], 0]], [[:[], 1]]],
33
+ # ["bar", [[:[], 1]], [[:[], 0]]]
34
+ # ]
35
+ def self.shared_mutables(obj1, obj2)
36
+ shared_objects(obj1, obj2, filter: method(:mutable?))
37
+ end
38
+
39
+ def self.shared_objects(obj1, obj2, filter: nil)
40
+ objects_with_path1 = call(obj1).map { |el, path| [el, path] }
41
+ objects_with_path2 = call(obj2).map { |el, path| [el, path] }
42
+ objects_with_path1.each.with_object([]) do |(el1, path1), acc|
43
+ next if filter && !filter.call(el1)
44
+
45
+ objects_with_path2.reject do |el2, path2|
46
+ acc << [el1, path1, path2] if el1.equal?(el2)
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.mutable?(obj)
52
+ !IMMUTABLE_CLASSES.include?(obj.class) && !obj.frozen?
53
+ end
54
+ private_class_method :mutable?
55
+
56
+ IMMUTABLE_CLASSES = [
57
+ FalseClass,
58
+ Float,
59
+ Integer,
60
+ NilClass,
61
+ Symbol,
62
+ TrueClass,
63
+ ].freeze
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leto
4
+ VERSION = "1.0.0"
5
+ end
data/lib/leto.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'leto/call'
4
+ require_relative 'leto/dig'
5
+ require_relative 'leto/path'
6
+ require_relative 'leto/utils'
7
+ require_relative 'leto/version'
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leto
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Janosch Müller
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - janosch84@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - ".rubocop.yml"
22
+ - CHANGELOG.md
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - benchmark.rb
28
+ - leto.gemspec
29
+ - lib/leto.rb
30
+ - lib/leto/call.rb
31
+ - lib/leto/dig.rb
32
+ - lib/leto/path.rb
33
+ - lib/leto/utils.rb
34
+ - lib/leto/version.rb
35
+ homepage: https://github.com/jaynetics/leto
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ changelog_uri: https://github.com/jaynetics/leto/blob/main/CHANGELOG.md
40
+ homepage_uri: https://github.com/jaynetics/leto
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.6.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.4.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Generic object traverser
60
+ test_files: []