leto 1.0.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.
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: []