foobara-util 0.0.4

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: 8d375fa882a7b34d39a4188c9c99b821770ab1edd6e843b78b9c4b5e3d755b4a
4
+ data.tar.gz: 4d8e3d0edd444bbc78139507cf8b7158e9d947717dd8dc1b83777393e62b3ca9
5
+ SHA512:
6
+ metadata.gz: 9fc109812e2f721642f6e9728f0cf942b5a75179f0b86e5cd8aeaac7a71339e07571aa055d8da7992bc87f411e42f5a7115a789fdf89daf38093264e3173ab2b
7
+ data.tar.gz: db0ac6f424dd18fdbf4a675d67caec1ac46b001513cc19278e7ead296f667331371d98850f6024e65f63b33b60c4304ae60d51808d717900df2f4bdbc1f5a6df
data/LICENSE.txt ADDED
@@ -0,0 +1,8 @@
1
+ foobara-util is dual licensed under your choice of the Apache-2.0 license and the MIT license.
2
+
3
+ Apache-2.0 License: LICENSE-APACHE.txt or https://www.apache.org/licenses/LICENSE-2.0.txt
4
+ MIT License: LICENSE-MIT.txt or https://opensource.org/licenses/MIT
5
+
6
+ This is equivalent to the following SPDX License Expression: Apache-2.0 OR MIT
7
+
8
+ Copyright (c) 2023 Miles Georgi
@@ -0,0 +1,65 @@
1
+ module Foobara
2
+ module TruncatedInspect
3
+ class << self
4
+ attr_accessor :truncated
5
+
6
+ def truncating(object)
7
+ if truncated
8
+ if truncated.include?(object)
9
+ "..."
10
+ else
11
+ truncated << object
12
+ yield
13
+ end
14
+ else
15
+ begin
16
+ self.truncated = Set[object]
17
+ yield
18
+ ensure
19
+ self.truncated = nil
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ MAX_LENGTH = 200
26
+
27
+ def inspect
28
+ TruncatedInspect.truncating(self) do
29
+ instance_vars = instance_variables.map do |var|
30
+ truncate = false
31
+
32
+ value = instance_variable_get(var)
33
+
34
+ is_array = value.is_a?(::Array)
35
+ is_hash = value.is_a?(::Hash)
36
+
37
+ if is_array
38
+ truncate = value.size > 10 || value.any? { |v| v.is_a?(::Array) || v.is_a?(::Hash) }
39
+ elsif is_hash
40
+ truncate = value.size > 10 || value.keys.any? { |k| !k.is_a?(::Symbol) && !k.is_a?(::String) }
41
+ truncate ||= value.values.any? { |v| v.is_a?(::Array) || v.is_a?(::Hash) }
42
+ end
43
+
44
+ value = if truncate
45
+ is_array ? "[...]" : "{...}"
46
+ else
47
+ value.inspect
48
+ end
49
+
50
+ "#{var}=#{value}"
51
+ end
52
+
53
+ instance_vars = instance_vars.join(", ")
54
+
55
+ result = "#<#{self.class.name}:0x#{object_id.to_s(16)} #{instance_vars}>"
56
+
57
+ if result.size > MAX_LENGTH
58
+ result = "#{result[0..(MAX_LENGTH - 5)]}...>"
59
+ end
60
+
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,105 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def args_and_opts_to_opts(args, opts)
6
+ unless args.is_a?(::Array)
7
+ # :nocov:
8
+ raise ArgumentError, "args must be an array of 0 or 1 hashes but received #{args}"
9
+ # :nocov:
10
+ end
11
+
12
+ unless opts.is_a?(::Hash)
13
+ # :nocov:
14
+ raise ArgumentError, "opts must be a hash not a #{opts.class}"
15
+ # :nocov:
16
+ end
17
+
18
+ case args.size
19
+ when 0
20
+ opts
21
+ when 1
22
+ arg = args.first
23
+
24
+ if opts && !opts.empty?
25
+ unless arg.is_a?(::Hash)
26
+ # :nocov:
27
+ raise ArgumentError, "opts must be a hash not a #{arg.class}"
28
+ # :nocov:
29
+ end
30
+
31
+ arg.merge(opts)
32
+ else
33
+ arg
34
+ end
35
+ else
36
+ # :nocov:
37
+ raise ArgumentError, "args must be an array of 0 or 1 hashes but received #{args}"
38
+ # :nocov:
39
+ end
40
+ end
41
+
42
+ def arg_and_opts_to_arg(arg, opts)
43
+ if arg && !arg.empty?
44
+ if opts && !opts.empty?
45
+ unless opts.is_a?(::Hash)
46
+ # :nocov:
47
+ raise ArgumentError, "opts must be a hash not a #{opts}"
48
+ # :nocov:
49
+ end
50
+
51
+ unless arg.is_a?(::Hash)
52
+ # :nocov:
53
+ raise ArgumentError, "arg must be a hash if present when opts is present"
54
+ # :nocov:
55
+ end
56
+
57
+ arg.merge(opts)
58
+ else
59
+ arg
60
+ end
61
+ elsif opts && !opts.empty?
62
+ opts
63
+ end
64
+ end
65
+
66
+ # Strange utility method here...
67
+ # idea is there's a method that takes 0 or 1 arguments which may or may not be a hash.
68
+ # To make sure we don't act like there's an argument when there's not or fail to combine
69
+ # parts of the hash that wind up in opts, this method comines it into an array of 0 or 1
70
+ # argument with stuff merged into the argument if needed.
71
+ def args_and_opts_to_args(args, opts)
72
+ unless args.is_a?(::Array)
73
+ # :nocov:
74
+ raise ArgumentError, "args must be an array of 0 or 1 hashes but received #{args}"
75
+ # :nocov:
76
+ end
77
+
78
+ case args.size
79
+ when 0
80
+ Util.array(arg_and_opts_to_arg(nil, opts))
81
+ when 1
82
+ # Do not go from 1 argument to 0. ie, [nil] should return [nil] not [].
83
+ if opts && !opts.empty?
84
+ arg = args.first
85
+
86
+ unless arg.is_a?(::Hash)
87
+ # :nocov:
88
+ raise ArgumentError, "Expected #{arg.inspect} to be a Hash"
89
+ # :nocov:
90
+ end
91
+
92
+ [arg_and_opts_to_arg(args.first, opts)]
93
+ else
94
+ args
95
+ end
96
+
97
+ [arg_and_opts_to_arg(args.first, opts)]
98
+ else
99
+ # :nocov:
100
+ raise ArgumentError, "args must be an array of 0 or 1 hashes but received #{args}"
101
+ # :nocov:
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,40 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def power_set(array)
6
+ return [[]] if array.empty?
7
+
8
+ head, *tail = array
9
+ subsets = power_set(tail)
10
+ subsets + subsets.map { |subset| [head, *subset] }
11
+ end
12
+
13
+ def array(object)
14
+ case object
15
+ when nil
16
+ []
17
+ when Array
18
+ object
19
+ else
20
+ [object]
21
+ end
22
+ end
23
+
24
+ def all_symbolic_elements?(array)
25
+ array.all? { |key| key.is_a?(Symbol) || key.is_a?(String) }
26
+ end
27
+
28
+ def all_symbolizable_elements?(array)
29
+ array.all? { |key| key.is_a?(Symbol) || key.is_a?(String) }
30
+ end
31
+
32
+ def all_blank_or_false?(array)
33
+ array.all? do |element|
34
+ element.nil? || element == false || (
35
+ (element.is_a?(::Hash) || element.is_a?(::Array) || element.is_a?(::String)) && element.empty?
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def descendants(klass)
6
+ all = Set.new
7
+
8
+ klass.subclasses.each do |subclass|
9
+ all << subclass
10
+ all |= descendants(subclass)
11
+ end
12
+
13
+ all
14
+ end
15
+
16
+ # WARNING: This approach is known as being slow. Probably better for you class to track its own instances
17
+ # if this is being used for smoething more than debugging.
18
+ def instances(klass)
19
+ ObjectSpace.each_object(klass).to_a
20
+ end
21
+
22
+ # Kind of surprising that Ruby doesn't have a built in way to do this.
23
+ def super_method_of(current_instance, from_class, method_name)
24
+ method = current_instance.method(method_name)
25
+ method = method.super_method until method.owner == from_class
26
+ method.super_method
27
+ end
28
+
29
+ def super_method_takes_parameters?(current_instance, from_class, method_name)
30
+ super_method_of(current_instance, from_class, method_name).parameters.any?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def symbolize_keys(hash)
6
+ unless all_symbolizable_keys?(hash)
7
+ # :nocov:
8
+ raise "Cannot symbolize keys for #{hash} because they are not all symbolizable"
9
+ # :nocov:
10
+ end
11
+
12
+ hash.transform_keys(&:to_sym)
13
+ end
14
+
15
+ def symbolize_keys!(hash)
16
+ unless all_symbolizable_keys?(hash)
17
+ # :nocov:
18
+ raise "Cannot symbolize keys for #{hash} because they are not all symbolizable"
19
+ # :nocov:
20
+ end
21
+
22
+ hash.transform_keys!(&:to_sym)
23
+ end
24
+
25
+ def all_symbolic_keys?(hash)
26
+ all_symbolic_elements?(hash.keys)
27
+ end
28
+
29
+ def all_symbolizable_keys?(hash)
30
+ all_symbolizable_elements?(hash.keys)
31
+ end
32
+
33
+ def remove_empty(hash)
34
+ hash.reject { |_k, v| (v.is_a?(::Hash) || v.is_a?(::Array)) && v.empty? }
35
+ end
36
+
37
+ def remove_blank(hash)
38
+ remove_empty(hash).compact
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,83 @@
1
+ module Foobara
2
+ module Util
3
+ class ParentModuleDoesNotExistError < StandardError
4
+ attr_reader :name, :parent_name
5
+
6
+ def initialize(name:, parent_name:)
7
+ @name = name
8
+ @parent_name = parent_name
9
+
10
+ super("Parent module #{parent_name} does not exist for #{name}.")
11
+ end
12
+ end
13
+
14
+ module_function
15
+
16
+ def make_class(name, superclass = nil, which: :class, tag: false, &block)
17
+ name = name.to_s if name.is_a?(::Symbol)
18
+
19
+ if superclass.is_a?(Class)
20
+ superclass = superclass.name
21
+ end
22
+
23
+ inherit = superclass ? " < ::#{superclass}" : ""
24
+
25
+ superclass ||= :Object
26
+
27
+ name = name[2..] if name.start_with?("::")
28
+
29
+ already_exists = Object.const_defined?(name, false)
30
+
31
+ if !already_exists && tag
32
+ should_tag = true
33
+ end
34
+
35
+ parent_name = parent_module_name_for(name)
36
+
37
+ if parent_name && !Object.const_defined?(parent_name, false)
38
+ raise ParentModuleDoesNotExistError.new(name:, parent_name:)
39
+ end
40
+
41
+ unless already_exists
42
+ # rubocop:disable Security/Eval, Style/DocumentDynamicEvalDefinition
43
+ eval(<<~RUBY, binding, __FILE__, __LINE__ + 1)
44
+ #{which} ::#{name}#{inherit}
45
+ end
46
+ RUBY
47
+ # rubocop:enable Security/Eval, Style/DocumentDynamicEvalDefinition
48
+ end
49
+
50
+ klass = Object.const_get(name, false)
51
+
52
+ if should_tag
53
+ klass.instance_variable_set(:@foobara_created_via_make_class, true)
54
+ end
55
+
56
+ if block
57
+ if klass.is_a?(::Class)
58
+ klass.class_eval(&block)
59
+ else
60
+ klass.module_eval(&block)
61
+ end
62
+ end
63
+
64
+ klass
65
+ end
66
+
67
+ def make_class_p(name, superclass = nil, which: :class, tag: false, &)
68
+ make_class(name, superclass, which:, tag:, &)
69
+ rescue ParentModuleDoesNotExistError => e
70
+ make_class_p(e.parent_name, which: :module, tag:, &)
71
+ make_class(name, superclass, which:, tag:, &)
72
+ end
73
+
74
+ # TODO: Kind of weird that make_module is implemented in terms of make_class instead of the other way around
75
+ def make_module(name, &)
76
+ make_class(name, which: :module, &)
77
+ end
78
+
79
+ def make_module_p(name, tag: false, &)
80
+ make_class_p(name, which: :module, tag:, &)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,94 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def module_for(mod)
6
+ name = mod.name
7
+
8
+ return unless name
9
+
10
+ name = parent_module_name_for(mod.name)
11
+ Object.const_get(name) if name
12
+ end
13
+
14
+ def parent_module_name_for(module_name)
15
+ module_name[/(.*)::/, 1]
16
+ end
17
+
18
+ def non_full_name(mod)
19
+ name = if mod.is_a?(::String)
20
+ mod
21
+ else
22
+ mod.name
23
+ end
24
+
25
+ name&.[](/([^:]+)\z/, 1)
26
+ end
27
+
28
+ def non_full_name_underscore(mod)
29
+ underscore(non_full_name(mod))
30
+ end
31
+
32
+ def constant_value(mod, constant, inherit: false)
33
+ if mod.constants(inherit).include?(constant.to_sym)
34
+ mod.const_get(constant, inherit)
35
+ end
36
+ end
37
+
38
+ def constant_values(mod, is_a: nil, extends: nil, inherit: false)
39
+ if inherit && !mod.is_a?(Class)
40
+ # :nocov:
41
+ raise "Cannot pass inherit: true for something that is not a Class"
42
+ # :nocov:
43
+ end
44
+
45
+ if inherit
46
+ superklass = mod.superclass
47
+ values = constant_values(mod, is_a:, extends:)
48
+ if superklass == Object
49
+ values
50
+ else
51
+ [
52
+ *values,
53
+ *constant_values(superklass, is_a:, extends:, inherit:)
54
+ ]
55
+ end
56
+ else
57
+ is_a = Util.array(is_a)
58
+ extends = Util.array(extends)
59
+
60
+ mod.constants.map { |const| constant_value(mod, const) }.select do |object|
61
+ (is_a.nil? || is_a.empty? || is_a.any? { |klass| object.is_a?(klass) }) &&
62
+ (extends.nil? || extends.empty? || (object.is_a?(Class) && extends.any? do |klass|
63
+ object.ancestors.include?(klass)
64
+ end))
65
+ end.compact
66
+ end
67
+ end
68
+
69
+ def const_get_up_hierarchy(mod, name)
70
+ mod.const_get(name)
71
+ rescue NameError => e
72
+ if mod == Object || e.message !~ /uninitialized constant (.*::)?#{name}\z/
73
+ # :nocov:
74
+ raise
75
+ # :nocov:
76
+ end
77
+
78
+ mod = if mod.name&.include?("::")
79
+ module_for(mod)
80
+ else
81
+ Object
82
+ end
83
+
84
+ const_get_up_hierarchy(mod, name)
85
+ end
86
+
87
+ def remove_constant(const_name)
88
+ *path, name = const_name.split("::")
89
+ mod = path.inject(Object) { |m, constant| m.const_get(constant) }
90
+
91
+ mod.send(:remove_const, name)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,23 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def require_directory(directory)
6
+ require_pattern("#{directory}/**/*.rb")
7
+ end
8
+
9
+ def require_pattern(glob)
10
+ files = Dir[glob]
11
+
12
+ if files.empty?
13
+ # :nocov:
14
+ raise "Didn't find anything to require for #{glob}"
15
+ # :nocov:
16
+ end
17
+
18
+ files.sort_by { |file| [file.count("/"), file.length] }.reverse.each do |f|
19
+ require f
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,125 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def classify(string)
6
+ camelize(string, true)
7
+ end
8
+
9
+ def camelize(string, upcase_first = false)
10
+ return nil if string.nil?
11
+
12
+ if string.is_a?(::Symbol)
13
+ string = string.to_s
14
+ end
15
+
16
+ retval = ""
17
+
18
+ string.each_char do |char|
19
+ if char == "_"
20
+ upcase_first = true
21
+ elsif upcase_first
22
+ retval << char.upcase
23
+ upcase_first = false
24
+ else
25
+ retval << char.downcase
26
+ end
27
+ end
28
+
29
+ retval
30
+ end
31
+
32
+ IS_CAP = /[A-Z]/
33
+ IS_IDENTIFIER_CHARACTER = /\w/
34
+
35
+ def constantify(string)
36
+ return nil if string.nil?
37
+
38
+ if string.is_a?(::Symbol)
39
+ string = string.to_s
40
+ end
41
+
42
+ if string =~ /\A[A-Z_]*\z/
43
+ string.dup
44
+ else
45
+ underscore(string).upcase
46
+ end
47
+ end
48
+
49
+ def constantify_sym(string)
50
+ constantify(string)&.to_sym
51
+ end
52
+
53
+ def underscore(string)
54
+ return nil if string.nil?
55
+
56
+ if string.is_a?(::Symbol)
57
+ string = string.to_s
58
+ end
59
+
60
+ return "" if string.empty?
61
+
62
+ retval = ""
63
+ is_start = true
64
+
65
+ string.each_char do |char|
66
+ if IS_IDENTIFIER_CHARACTER =~ char
67
+ if IS_CAP =~ char
68
+ char = char.downcase
69
+ char = "_#{char}" unless is_start
70
+ is_start = false
71
+ elsif char == "_"
72
+ is_start = true
73
+ else
74
+ is_start = false
75
+ end
76
+ else
77
+ is_start = true
78
+ end
79
+
80
+ retval << char
81
+ end
82
+
83
+ retval
84
+ end
85
+
86
+ def kebab_case(string)
87
+ underscore(string)&.gsub("_", "-")
88
+ end
89
+
90
+ def underscore_sym(string)
91
+ underscore(string)&.to_sym
92
+ end
93
+
94
+ def to_sentence(strings, connector = ", ", last_connector = ", and ")
95
+ return "" if strings.empty?
96
+
97
+ *strings, last = strings
98
+
99
+ if strings.empty?
100
+ last
101
+ else
102
+ [strings.join(connector), last].join(last_connector)
103
+ end
104
+ end
105
+
106
+ def to_or_sentence(strings, connector = ", ")
107
+ to_sentence(strings, connector, ", or ")
108
+ end
109
+
110
+ def humanize(string)
111
+ return nil if string.nil?
112
+
113
+ if string.is_a?(::Symbol)
114
+ string = string.to_s
115
+ end
116
+
117
+ return "" if string.empty?
118
+
119
+ string = string.gsub("_", " ")
120
+ string[0] = string[0].upcase
121
+
122
+ string
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,33 @@
1
+ module Foobara
2
+ module Util
3
+ module_function
4
+
5
+ def deep_stringify_keys(object)
6
+ case object
7
+ when ::Array
8
+ object.map { |element| deep_stringify_keys(element) }
9
+ when ::Hash
10
+ object.to_h do |k, v|
11
+ [k.is_a?(::Symbol) ? k.to_s : k, deep_stringify_keys(v)]
12
+ end
13
+ else
14
+ object
15
+ end
16
+ end
17
+
18
+ def deep_dup(object)
19
+ case object
20
+ when ::Array
21
+ object.map { |element| deep_dup(element) }
22
+ when ::Hash
23
+ object.to_h { |k, v| [deep_dup(k), deep_dup(v)] }
24
+ when ::String
25
+ # Important to not dup ::Module... but going to exclude everything but String for now to prevent that issue
26
+ # and others.
27
+ object.dup
28
+ else
29
+ object
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Foobara
2
+ module Util
3
+ VERSION = "0.0.4".freeze
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ Dir["#{__dir__}/../**/*.rb"].each { |f| require f }
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foobara-util
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Miles Georgi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - azimux@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - lib/foobara/truncated_inspect.rb
22
+ - lib/foobara/util.rb
23
+ - lib/foobara/util/args.rb
24
+ - lib/foobara/util/array.rb
25
+ - lib/foobara/util/class.rb
26
+ - lib/foobara/util/hash.rb
27
+ - lib/foobara/util/meta.rb
28
+ - lib/foobara/util/module.rb
29
+ - lib/foobara/util/require.rb
30
+ - lib/foobara/util/string.rb
31
+ - lib/foobara/util/structured.rb
32
+ - lib/foobara/util/version.rb
33
+ homepage: https://github.com/foobara/util
34
+ licenses:
35
+ - Apache-2.0
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/foobara/util
39
+ source_code_uri: https://github.com/foobara/util
40
+ changelog_uri: https://github.com/foobara/util/blob/main/CHANGELOG.md
41
+ rubygems_mfa_required: 'true'
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: 3.2.2
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.4.10
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Utility functions used across various Foobara projects
61
+ test_files: []