leto 1.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 539f52438c18bb93115734b5c3c9dd5f8678886d392357c4fdca658b43d95357
4
- data.tar.gz: bbe092a276d3241d39857cb81e8fb748108b15e67cbc0e421b7390545b8a066a
3
+ metadata.gz: 0ed0d190b234b4976a219ba5b50a3e9cb4f394be7b1a27aa2f1d7bdfe93887b0
4
+ data.tar.gz: eafafd24510514cf7ba4e59cdf1a967c7464d175b8ceed1f3cafa2e78fffcc1e
5
5
  SHA512:
6
- metadata.gz: 9a0e610d258c35cf044c0118ccc10dff918edc8495df7448c058dbe6ad3201e540952fa67ac14e065229e1dd961788936e1343d30254743f47319609b5b4e169
7
- data.tar.gz: c7e0fa4e9be9c2c0da092a7dbb88c78b9b11a12d8fb4f9d34e2ed97794ab2f26282c1fe31b04db5c70039198e675d2770ab0a7ae52edcb28049e7568c6c7bc56
6
+ metadata.gz: 79b0be7884c86be4a14977e5aaae6147da85b857f632013bd82f9fe95addf28a4014ea7adeba1c75dfc63b9988ab7426c622cb79bc2c2886dca04be246ee2db5
7
+ data.tar.gz: 961a19be85b69aac0763828648c09100af4a9f84eff07a45e2b9c0c247c84e5b7ae9647a9bd4a7de791eb7cd0f0342b91f82d2cd50a85286425e4a8f915bd2fe
data/.rubocop.yml CHANGED
@@ -21,8 +21,14 @@ Lint/AmbiguousBlockAssociation:
21
21
  Lint/AmbiguousOperatorPrecedence:
22
22
  Enabled: false
23
23
 
24
+ Style/DocumentDynamicEvalDefinition:
25
+ Enabled: false
26
+
24
27
  Style/FetchEnvVar:
25
28
  Enabled: false
26
29
 
27
30
  Style/FrozenStringLiteralComment:
28
31
  Enabled: false
32
+
33
+ Style/SymbolProc:
34
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.1.0] - 2023-05-20
4
+
5
+ ### Added
6
+
7
+ - `::call`, `::trace` and `::deep_dup` support for Data feature of Ruby 3.2
8
+
9
+ ## [2.0.0] - 2023-02-27
10
+
11
+ ### Changed
12
+
13
+ - removed support for getting paths by calling `::call` with a two-arg block
14
+
15
+ ### Added
16
+
17
+ - `::deep_dup`
18
+ - `::trace`
19
+ - support for older Rubies
20
+
3
21
  ## [1.0.0] - 2023-02-24
4
22
 
5
23
  - Initial release
data/Gemfile CHANGED
@@ -6,9 +6,10 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
-
10
9
  gem "rspec", "~> 3.0"
11
10
 
12
- gem "rubocop", "~> 1.21"
13
-
14
- gem "relaxed-rubocop"
11
+ if RUBY_VERSION.to_f >= 3.0
12
+ gem "relaxed-rubocop"
13
+ gem "rubocop", "~> 1.21"
14
+ gem "simplecov-cobertura"
15
+ end
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Leto
2
2
 
3
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)
4
+ [![Build Status](https://github.com/jaynetics/leto/actions/workflows/tests.yml/badge.svg)](https://github.com/jaynetics/leto/actions)
5
+ [![Coverage](https://codecov.io/gh/jaynetics/leto/branch/main/graph/badge.svg?token=0993K9I8VC)](https://codecov.io/gh/jaynetics/leto)
5
6
 
6
7
  A generic object traverser for Ruby (named after the Greek [childbearing goddess Leto](https://www.theoi.com/Titan/TitanisLeto.html)).
7
8
 
@@ -12,6 +13,7 @@ Takes an object and recursively yields:
12
13
  - Hash keys and values
13
14
  - Enumerable members
14
15
  - Struct members
16
+ - [Data](https://docs.ruby-lang.org/en/3.2/Data.html) members
15
17
  - Range begins and ends
16
18
 
17
19
  This makes stuff like deep-freezing fairly easy to implement:
@@ -50,8 +52,8 @@ Leto.call(object) { |el| p el }
50
52
  Leto.call(object).to_a
51
53
  # => [[{:a=>["b", ["c".."d"]]}], {:a=>["b", ["c".."d"]]}, :a, ...]
52
54
 
53
- # all (sub-)objects have a path:
54
- Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
+ # Leto::trace behaves like ::call, but also yields each (sub-)object's path:
56
+ Leto.trace(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
55
57
  # prints:
56
58
  #
57
59
  # [{:a=>["b", "c".."d"]}] @#<Leto::Path [{:a=>["b", "c".."d"]}]>
@@ -64,7 +66,7 @@ Leto.call(object) { |el, path| puts "#{el.inspect.ljust(23)} @#{path}" }
64
66
  # "d" @#<Leto::Path [{:a=>["b", "c".."d"]}][0][:a][1].end>
65
67
 
66
68
  # paths can be looked up with Leto::Path#resolve or Leto::dig
67
- path = Leto.call(object).map { |_el, path| path }.last # => #<Leto::Path...>
69
+ path = Leto.trace(object).map { |_el, path| path }.last # => #<Leto::Path...>
68
70
  path.resolve # => "d"
69
71
  Leto.dig(object, path) # => "d"
70
72
  Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
@@ -72,17 +74,19 @@ Leto.dig(object, [[:[], 0], [:[], :a], [:[], 1], [:end]]) # => "d"
72
74
 
73
75
  ### Included utility methods
74
76
 
75
- - `Leto.deep_freeze(obj)`
77
+ - [`Leto.deep_freeze(obj)`](https://github.com/search?q=deep_freeze+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
76
78
  - similar to the version above, but avoids freezing Modules and unfreezables
77
- - `Leto.deep_print(obj)`
79
+ - [`Leto.deep_print(obj)`](https://github.com/search?q=deep_print+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
78
80
  - for debugging - prints more information than `pretty_print` does by default
79
- - `Leto.deep_eql?(obj1, obj2)`
81
+ - [`Leto.deep_eql?(obj1, obj2)`](https://github.com/search?q=deep_eql+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
80
82
  - stricter version of `#eql?` that takes all ivars into consideration
81
- - `Leto.shared_mutable_state?(obj1, obj2)`
83
+ - [`Leto.deep_dup(obj)`](https://github.com/search?q=deep_dup+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
84
+ - more thorough than [`active_support`](https://www.rubydoc.info/search/gems/activesupport?q=deep_dup) or [`deep_dup`](https://github.com/ollie/deep_dup) gems, e.g. dups ivars
85
+ - [`Leto.shared_mutable_state?(obj1, obj2)`](https://github.com/search?q=shared_mutable_state+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
82
86
  - useful for debugging or verifying that a `#dup` implementation is sane
83
- - `Leto.shared_mutables(obj1, obj2)`
87
+ - [`Leto.shared_mutables(obj1, obj2)`](https://github.com/search?q=shared_mutables+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
84
88
  - useful for debugging or verifying that a `#dup` implementation is sane
85
- - `Leto.shared_objects(obj1, obj2)`
89
+ - [`Leto.shared_objects(obj1, obj2)`](https://github.com/search?q=shared_objects+repo%3Ajaynetics%2Fleto+path%3Alib%2Fleto%2Futils.rb&type=code)
86
90
  - returns all objects shared by `obj1` and `obj2`, whether mutable or not
87
91
 
88
92
  ## Benchmarks
data/Rakefile CHANGED
@@ -5,8 +5,10 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
8
+ if RUBY_VERSION.to_f >= 3.0
9
+ require "rubocop/rake_task"
10
+ RuboCop::RakeTask.new
11
+ task default: %i[spec rubocop]
12
+ else
13
+ task default: %i[spec]
14
+ end
data/leto.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Generic object traverser"
12
12
  spec.homepage = "https://github.com/jaynetics/leto"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 2.6.0"
14
+ spec.required_ruby_version = ">= 2.3.0"
15
15
 
16
16
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
17
17
  spec.metadata["homepage_uri"] = spec.homepage
data/lib/leto/call.rb CHANGED
@@ -1,64 +1,92 @@
1
1
  module Leto
2
- LEAKY_PROCESS_TMS = RUBY_VERSION.to_f <= 2.7
2
+ def self.call(obj, max_depth: nil, path: nil, &block)
3
+ block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
3
4
 
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)
5
+ traverse(obj, path, 0, max_depth, build_seen_hash, block)
11
6
  obj
12
7
  end
13
8
 
14
- def self.traverse(obj, path, seen, block)
15
- return if seen[obj]
9
+ def self.trace(obj, max_depth: nil, path: nil, &block)
10
+ block_given? or return enum_for(__method__, obj, max_depth: max_depth, path: path)
16
11
 
17
- seen[obj] = true
12
+ call(obj, max_depth: max_depth, path: path || Path.new(start: obj), &block)
13
+ end
18
14
 
19
- path ? block.call(obj, path) : block.call(obj)
15
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
20
16
 
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
17
+ private
28
18
 
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|
19
+ def traverse(obj, path, depth, max_depth, seen, block)
20
+ return if seen[obj] || max_depth&.<(depth)
21
+
22
+ seen[obj] = true
23
+ depth += 1
24
+
25
+ path ? block.call(obj, path) : block.call(obj)
26
+
27
+ obj.instance_variables.each do |ivar_name|
44
28
  traverse(
45
- obj.const_get(const_name),
46
- path&.+([[:const_get, const_name]]),
47
- seen, block
29
+ obj.instance_variable_get(ivar_name),
30
+ path&.+([[:instance_variable_get, ivar_name]]),
31
+ depth, max_depth, seen, block
48
32
  )
49
33
  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)
34
+
35
+ case obj
36
+ when Hash
37
+ obj.keys.each_with_index do |k, i|
38
+ traverse(k, path&.+([[:keys], [:[], i]]), depth, max_depth, seen, block)
39
+ traverse(obj[k], path&.+([[:[], k]]), depth, max_depth, seen, block)
40
+ end
41
+ when Module
42
+ obj.class_variables.each do |cvar_name|
43
+ traverse(
44
+ obj.class_variable_get(cvar_name),
45
+ path&.+([[:class_variable_get, cvar_name]]),
46
+ depth, max_depth, seen, block
47
+ )
48
+ end
49
+ obj.constants.each do |const_name|
50
+ traverse(
51
+ obj.const_get(const_name),
52
+ path&.+([[:const_get, const_name]]),
53
+ depth, max_depth, seen, block
54
+ )
55
+ end
56
+ when Range
57
+ traverse(obj.begin, path&.+([[:begin]]), depth, max_depth, seen, block)
58
+ traverse(obj.end, path&.+([[:end]]), depth, max_depth, seen, block)
59
+ when Struct
60
+ obj.members.each do |member|
61
+ traverse(obj[member], path&.+([[:[], member]]), depth, max_depth, seen, block)
62
+ end
63
+ when Enumerable
64
+ obj.each_with_index do |el, idx|
65
+ traverse(el, path&.+([[:[], idx]]), depth, max_depth, seen, block)
66
+ end
67
+ #{
68
+ defined?(Data) && Data.respond_to?(:define) && <<-DATA_FEATURE_RUBY
69
+ when Data
70
+ obj.members.each do |member|
71
+ traverse(
72
+ obj.send(member),
73
+ path&.+([[:send, member]]),
74
+ depth, max_depth, seen, block
75
+ )
76
+ end
77
+ DATA_FEATURE_RUBY
78
+ }
60
79
  end
61
80
  end
62
- end
63
- private_class_method :traverse
81
+
82
+ # ignore leaky constants in old rubies
83
+ def build_seen_hash
84
+ hash = {}
85
+ hash.compare_by_identity
86
+ #{'hash[::Etc::Group] = true' if defined?(::Etc::Group)}
87
+ #{'hash[::Etc::Passwd] = true' if defined?(::Etc::Passwd)}
88
+ #{'hash[::Process::Tms] = true' if defined?(::Process::Tms)}
89
+ hash
90
+ end
91
+ RUBY
64
92
  end
data/lib/leto/path.rb CHANGED
@@ -14,6 +14,11 @@ module Leto
14
14
  steps.each(&block)
15
15
  end
16
16
 
17
+ def size
18
+ steps.size
19
+ end
20
+ alias count size
21
+
17
22
  def +(other)
18
23
  self.class.new(start: start, steps: steps + other.to_a)
19
24
  end
@@ -21,8 +26,6 @@ module Leto
21
26
  def resolve
22
27
  steps.inject(start) do |obj, (method, *args)|
23
28
  obj&.send(method, *args) or break obj
24
- rescue StandardError => e
25
- warn "#{__method__}: #{e.class} #{e.message}"
26
29
  end
27
30
  end
28
31
 
data/lib/leto/utils.rb CHANGED
@@ -8,7 +8,7 @@ module Leto
8
8
  end
9
9
 
10
10
  def self.deep_print(obj, print_method: :inspect, indent: 4, show_path: true)
11
- call(obj) do |el, path|
11
+ trace(obj) do |el, path|
12
12
  puts "#{' ' * path.count * indent}#{el.send(print_method)}" \
13
13
  "#{" @ #{path.inspect}" if show_path}" \
14
14
  [0..78]
@@ -20,8 +20,31 @@ module Leto
20
20
  call(obj1).to_a == call(obj2).to_a
21
21
  end
22
22
 
23
+ def self.deep_dup(obj, include_modules: false)
24
+ return obj if IMMUTABLE_CLASSES.include?(obj.class) || !duplicable?(obj) ||
25
+ (!include_modules && obj.is_a?(Module))
26
+
27
+ copy = obj.dup
28
+
29
+ trace(obj, max_depth: 1).each do |el, path|
30
+ method, *args = path.steps[0]
31
+ case method
32
+ when :instance_variable_get
33
+ copy.instance_variable_set(*args, deep_dup(el, include_modules: include_modules))
34
+ when :[]
35
+ copy[*args] = deep_dup(el, include_modules: include_modules)
36
+ when :send # Data
37
+ copy = copy.with(args[0] => deep_dup(el, include_modules: include_modules))
38
+ when :begin
39
+ return Range.new(deep_dup(obj.begin), deep_dup(obj.end), obj.exclude_end?)
40
+ end
41
+ end
42
+
43
+ copy
44
+ end
45
+
23
46
  def self.shared_mutable_state?(obj1, obj2)
24
- shared_mutables(obj1, obj2).any?
47
+ each_shared_object(obj1, obj2, filter: method(:mutable?)).any?
25
48
  end
26
49
 
27
50
  # returns [[shared_object, path1, path2], ...], e.g.:
@@ -33,17 +56,22 @@ module Leto
33
56
  # ["bar", [[:[], 1]], [[:[], 0]]]
34
57
  # ]
35
58
  def self.shared_mutables(obj1, obj2)
36
- shared_objects(obj1, obj2, filter: method(:mutable?))
59
+ each_shared_object(obj1, obj2, filter: method(:mutable?)).to_a
37
60
  end
38
61
 
39
62
  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|
63
+ each_shared_object(obj1, obj2, filter: filter).to_a
64
+ end
65
+
66
+ def self.each_shared_object(obj1, obj2, filter: nil)
67
+ block_given? or return enum_for(__method__, obj1, obj2, filter: filter)
68
+
69
+ obj2_els_with_path = trace(obj2).to_a
70
+ trace(obj1).each do |el1, path1|
43
71
  next if filter && !filter.call(el1)
44
72
 
45
- objects_with_path2.reject do |el2, path2|
46
- acc << [el1, path1, path2] if el1.equal?(el2)
73
+ obj2_els_with_path.reject do |el2, path2|
74
+ yield(el1, path1, path2) if el1.equal?(el2)
47
75
  end
48
76
  end
49
77
  end
@@ -56,9 +84,26 @@ module Leto
56
84
  IMMUTABLE_CLASSES = [
57
85
  FalseClass,
58
86
  Float,
59
- Integer,
87
+ if defined?(Integer)
88
+ Integer
89
+ else
90
+ Fixnum # rubocop:disable Lint/UnifiedInteger for Ruby < 2.4
91
+ end,
60
92
  NilClass,
61
93
  Symbol,
62
94
  TrueClass,
63
95
  ].freeze
96
+
97
+ def self.duplicable?(obj)
98
+ !NON_DUPLICABLE_CLASSES.include?(obj.class) && obj.respond_to?(:dup)
99
+ end
100
+ private_class_method :duplicable?
101
+
102
+ require 'singleton'
103
+
104
+ NON_DUPLICABLE_CLASSES = [
105
+ Method,
106
+ Singleton,
107
+ UnboundMethod
108
+ ].freeze
64
109
  end
data/lib/leto/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leto
4
- VERSION = "1.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leto
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-24 00:00:00.000000000 Z
11
+ date: 2023-05-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -46,14 +46,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 2.6.0
49
+ version: 2.3.0
50
50
  required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubygems_version: 3.4.1
56
+ rubygems_version: 3.4.13
57
57
  signing_key:
58
58
  specification_version: 4
59
59
  summary: Generic object traverser