sevgi-function 0.93.1 → 0.94.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE +5 -0
- data/README.md +33 -2
- data/lib/sevgi/core.rb +0 -15
- data/lib/sevgi/function/locate.rb +55 -31
- data/lib/sevgi/function/math.rb +7 -2
- data/lib/sevgi/function/shell.rb +94 -25
- data/lib/sevgi/function/string.rb +11 -13
- data/lib/sevgi/function/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7afe91e24bcfcbc6b2e398838f8cf9cce6fdb97b9c7b7cded831a6f892c75280
|
|
4
|
+
data.tar.gz: 505afa54ddc3e02573ed7169ca6aa01f14a90bd3c57d41bafe0cb87bb7b47e46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd9066dc9cc025ff15122265b66ace4861a62b150a10ac72addd1255bf3223337cd1ad3c6e0259423abc184a98db97cb0f5c0e0769224240e20db8ae8654ee41
|
|
7
|
+
data.tar.gz: 0d47888b1cce2c39fba38c09ead6044401908ae48d789ff596ee0cb7055c2225cab69b094803c84a8a8d1737d4d2a3268e1f769154e9770c55c01e9813f6160e
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
data/README.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Sevgi Function
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared helper functions used by Sevgi components.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install sevgi-function
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Require
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "sevgi/function"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Sevgi::F.eq?(0.1 + 0.2, 0.3, precision: 12)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Ruby compatibility
|
|
24
|
+
|
|
25
|
+
Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the current development Ruby from `.ruby-version`.
|
|
26
|
+
|
|
27
|
+
## Native prerequisites
|
|
28
|
+
|
|
29
|
+
None beyond Ruby and this gem's Ruby dependencies.
|
|
30
|
+
|
|
31
|
+
## Links
|
|
32
|
+
|
|
33
|
+
- Documentation: https://sevgi.roktas.dev
|
|
34
|
+
- API documentation: https://www.rubydoc.info/gems/sevgi-function
|
|
35
|
+
- Source: https://github.com/roktas/sevgi/tree/main/function
|
|
36
|
+
- Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
|
data/lib/sevgi/core.rb
CHANGED
|
@@ -45,21 +45,6 @@ module Sevgi
|
|
|
45
45
|
# Error raised for invalid public API usage.
|
|
46
46
|
ArgumentError = Class.new(Error) unless defined?(self::ArgumentError)
|
|
47
47
|
|
|
48
|
-
# Shared immutable empty array.
|
|
49
|
-
EMPTY_ARRAY = [].freeze
|
|
50
|
-
|
|
51
|
-
# Shared immutable empty hash.
|
|
52
|
-
EMPTY_HASH = {}.freeze
|
|
53
|
-
|
|
54
|
-
# Shared immutable empty options hash.
|
|
55
|
-
EMPTY_OPTS = {}.freeze
|
|
56
|
-
|
|
57
|
-
# Shared empty string.
|
|
58
|
-
EMPTY_STRING = ""
|
|
59
|
-
|
|
60
|
-
# Identity callable.
|
|
61
|
-
IDENTITY = -> (x) { x }.freeze
|
|
62
|
-
|
|
63
48
|
# Sentinel object used to distinguish an omitted value from nil.
|
|
64
49
|
Undefined = Object
|
|
65
50
|
.new
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
+
# Found file location returned by locate helpers.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] file
|
|
8
|
+
# @return [String] absolute matching file path
|
|
9
|
+
# @!attribute [r] slug
|
|
10
|
+
# @return [String] candidate path that matched
|
|
11
|
+
# @!attribute [r] dir
|
|
12
|
+
# @return [String] directory where the match was found
|
|
13
|
+
Location = Data.define(:file, :slug, :dir)
|
|
14
|
+
|
|
5
15
|
# Locates one of several candidate files by walking upward from a start directory.
|
|
6
16
|
class Locate
|
|
7
17
|
# @overload call(paths, start = Dir.pwd, exclude: nil, &block)
|
|
@@ -12,64 +22,78 @@ module Sevgi
|
|
|
12
22
|
# @yield optional matcher used instead of file existence checks
|
|
13
23
|
# @yieldparam path [String] candidate path
|
|
14
24
|
# @yieldreturn [Boolean]
|
|
15
|
-
# @return [Sevgi::Function::
|
|
25
|
+
# @return [Sevgi::Function::Location, nil] found location, or nil
|
|
16
26
|
def self.call(*, **, &block) = new(*, **).call(&block)
|
|
17
27
|
|
|
18
|
-
Location = Data.define(:file, :slug, :dir)
|
|
19
|
-
|
|
20
|
-
private_constant :Location
|
|
21
|
-
|
|
22
28
|
# @!attribute [r] paths
|
|
23
29
|
# @return [Array<String>] candidate paths
|
|
24
30
|
# @!attribute [r] start
|
|
25
|
-
# @return [String] start directory
|
|
31
|
+
# @return [String] absolute start directory
|
|
26
32
|
# @!attribute [r] exclude
|
|
27
33
|
# @return [Array<String>, nil] expanded paths ignored during lookup
|
|
28
34
|
attr_reader :paths, :start, :exclude
|
|
29
35
|
|
|
30
36
|
# Builds an upward file locator.
|
|
31
37
|
# @param paths [Array<String>, String] candidate file paths
|
|
32
|
-
# @param start [String] directory where lookup starts
|
|
33
|
-
# @param exclude [Array<String>, String, nil] paths ignored during lookup
|
|
38
|
+
# @param start [String] directory where lookup starts, expanded without changing process cwd
|
|
39
|
+
# @param exclude [Array<String>, String, nil] paths ignored during lookup after absolute expansion
|
|
34
40
|
# @return [void]
|
|
35
41
|
def initialize(paths, start = ::Dir.pwd, exclude: nil)
|
|
36
42
|
@paths = Array(paths)
|
|
37
|
-
@start = start
|
|
43
|
+
@start = ::File.expand_path(start)
|
|
38
44
|
@exclude = [*exclude].map { ::File.expand_path(it) } unless exclude.nil?
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
# Runs the upward lookup.
|
|
42
48
|
# @yield optional matcher used instead of file existence checks
|
|
43
|
-
# @yieldparam path [String] candidate path
|
|
49
|
+
# @yieldparam path [String] absolute candidate path
|
|
44
50
|
# @yieldreturn [Boolean]
|
|
45
|
-
# @return [Sevgi::Function::
|
|
46
|
-
# @raise [Errno::ENOENT] when the start directory
|
|
51
|
+
# @return [Sevgi::Function::Location, nil] found location, or nil
|
|
52
|
+
# @raise [Errno::ENOENT] when the start directory does not exist
|
|
53
|
+
# @raise [Errno::ENOTDIR] when the start path is not a directory
|
|
47
54
|
def call(&block)
|
|
48
|
-
|
|
49
|
-
::Dir.chdir(start)
|
|
55
|
+
validate_start!
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end
|
|
57
|
+
each_parent(start) do |here|
|
|
58
|
+
next unless (found = match(here, &block))
|
|
59
|
+
|
|
60
|
+
slug, file = found
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
::Dir.chdir(origin)
|
|
62
|
+
return Location[file, slug, here]
|
|
63
|
+
end
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
private
|
|
63
67
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
def each_parent(here)
|
|
69
|
+
loop do
|
|
70
|
+
yield here
|
|
71
|
+
|
|
72
|
+
parent = ::File.dirname(here)
|
|
73
|
+
break if parent == here
|
|
74
|
+
|
|
75
|
+
here = parent
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def excluded?(candidate)
|
|
80
|
+
exclude&.include?(candidate)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def match(here, &block)
|
|
84
|
+
paths.each do |path|
|
|
85
|
+
candidate = ::File.expand_path(path, here)
|
|
86
|
+
next if !block && excluded?(candidate)
|
|
87
|
+
|
|
88
|
+
return [path, candidate] if (block || proc { ::File.exist?(it) }).call(candidate)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
def validate_start!
|
|
95
|
+
raise ::Errno::ENOENT, start unless ::File.exist?(start)
|
|
96
|
+
raise ::Errno::ENOTDIR, start unless ::File.directory?(start)
|
|
73
97
|
end
|
|
74
98
|
end
|
|
75
99
|
|
|
@@ -78,7 +102,7 @@ module Sevgi
|
|
|
78
102
|
# @param start [String] directory where lookup starts
|
|
79
103
|
# @param exclude [Array<String>, String, nil] paths ignored during lookup
|
|
80
104
|
# @param extension [String] default extension added before lookup
|
|
81
|
-
# @return [Sevgi::Function::
|
|
105
|
+
# @return [Sevgi::Function::Location] found location
|
|
82
106
|
# @raise [Sevgi::Error] when no matching file exists
|
|
83
107
|
def self.locate(filename, start, exclude: nil, extension: EXTENSION)
|
|
84
108
|
Locate.(F.qualify(filename, extension), start, exclude:).tap do |path|
|
data/lib/sevgi/function/math.rb
CHANGED
|
@@ -77,8 +77,13 @@ module Sevgi
|
|
|
77
77
|
# @param length [Numeric] total length
|
|
78
78
|
# @param division [Numeric] division size
|
|
79
79
|
# @return [Integer]
|
|
80
|
-
# @raise [
|
|
81
|
-
def count(length, division)
|
|
80
|
+
# @raise [Sevgi::ArgumentError] when division is zero
|
|
81
|
+
def count(length, division)
|
|
82
|
+
divisor = division.to_f
|
|
83
|
+
ArgumentError.("Division must not be zero") if divisor.zero?
|
|
84
|
+
|
|
85
|
+
(length / divisor).to_i
|
|
86
|
+
end
|
|
82
87
|
|
|
83
88
|
# Compares two numeric values after approximate rounding.
|
|
84
89
|
# @param left [Numeric] left operand
|
data/lib/sevgi/function/shell.rb
CHANGED
|
@@ -6,17 +6,17 @@ module Sevgi
|
|
|
6
6
|
module Function
|
|
7
7
|
# Shell execution helpers and executable lookup utilities.
|
|
8
8
|
module Shell
|
|
9
|
-
# Checks whether a program exists
|
|
10
|
-
# @param program [Object] program name
|
|
11
|
-
# @return [Boolean] true when an executable
|
|
9
|
+
# Checks whether a program exists and is executable.
|
|
10
|
+
# @param program [Object] program name, absolute path, or relative slash-containing path
|
|
11
|
+
# @return [Boolean] true when an executable regular file is found
|
|
12
|
+
# @note PATH is evaluated on every call; empty PATH segments mean the current directory.
|
|
12
13
|
def executable?(program)
|
|
13
14
|
program = program.to_s
|
|
14
15
|
return false if program.empty?
|
|
16
|
+
return executable_file?(program) if slash_path?(program)
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
::File.executable?(::File.join(dir, program))
|
|
19
|
-
end
|
|
18
|
+
ENV.fetch("PATH", "").split(::File::PATH_SEPARATOR, -1).any? do |dir|
|
|
19
|
+
executable_file?(::File.join(dir.empty? ? "." : dir, program))
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -91,12 +91,9 @@ module Sevgi
|
|
|
91
91
|
def call(*args, &input)
|
|
92
92
|
return Result.dummy if args.empty?
|
|
93
93
|
|
|
94
|
+
@coathooks = 0
|
|
94
95
|
outs, errs, status = Open3.popen3(*args) do |stdin, stdout, stderr, wait_thread|
|
|
95
|
-
|
|
96
|
-
stdin.write(content) if content
|
|
97
|
-
stdin.close
|
|
98
|
-
|
|
99
|
-
capture(stdout, stderr, wait_thread)
|
|
96
|
+
capture(stdin, stdout, stderr, wait_thread, &input)
|
|
100
97
|
end
|
|
101
98
|
|
|
102
99
|
Result.new(args, outs, errs, status.exitstatus)
|
|
@@ -104,19 +101,47 @@ module Sevgi
|
|
|
104
101
|
|
|
105
102
|
private
|
|
106
103
|
|
|
107
|
-
|
|
104
|
+
# rubocop:disable Lint/RescueException
|
|
105
|
+
def capture(stdin, stdout, stderr, wait_thread, &input)
|
|
108
106
|
# Handle `^C`
|
|
109
107
|
previous = trap("INT") { handle_sigint(wait_thread.pid) }
|
|
108
|
+
readers = start_readers(stdout, stderr)
|
|
110
109
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
read_process(stdin, wait_thread, readers, &input)
|
|
111
|
+
rescue Exception
|
|
112
|
+
cleanup_failed_capture(stdin, wait_thread, readers)
|
|
113
|
+
raise
|
|
115
114
|
ensure
|
|
115
|
+
close_input(stdin)
|
|
116
116
|
trap("INT", previous) if previous
|
|
117
117
|
end
|
|
118
|
+
# rubocop:enable Lint/RescueException
|
|
119
|
+
|
|
120
|
+
def start_readers(stdout, stderr)
|
|
121
|
+
[
|
|
122
|
+
Thread.new { stdout.readlines.map(&:chomp) },
|
|
123
|
+
Thread.new { stderr.readlines.map(&:chomp) }
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def read_process(stdin, wait_thread, readers, &input)
|
|
128
|
+
write_input(stdin, &input)
|
|
129
|
+
close_input(stdin)
|
|
130
|
+
|
|
131
|
+
status = wait_thread.value
|
|
132
|
+
|
|
133
|
+
[readers[0].value, readers[1].value, status]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def cleanup_failed_capture(stdin, wait_thread, readers)
|
|
137
|
+
close_input(stdin)
|
|
138
|
+
stop_process(wait_thread)
|
|
139
|
+
join_readers(readers)
|
|
140
|
+
end
|
|
118
141
|
|
|
119
142
|
def handle_sigint(pid)
|
|
143
|
+
@coathooks += 1
|
|
144
|
+
|
|
120
145
|
message, signal = if @coathooks > 1
|
|
121
146
|
["SIGINT received again. Force quitting...", "KILL"]
|
|
122
147
|
else
|
|
@@ -126,28 +151,66 @@ module Sevgi
|
|
|
126
151
|
warn
|
|
127
152
|
warn(message)
|
|
128
153
|
::Process.kill(signal, pid)
|
|
129
|
-
@coathooks += 1
|
|
130
154
|
rescue Errno::ESRCH
|
|
131
155
|
warn("No process to kill.")
|
|
132
156
|
end
|
|
157
|
+
|
|
158
|
+
def write_input(stdin)
|
|
159
|
+
return unless block_given?
|
|
160
|
+
|
|
161
|
+
content = yield
|
|
162
|
+
stdin.write(content) if content
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def close_input(stdin)
|
|
166
|
+
stdin.close unless stdin.closed?
|
|
167
|
+
rescue IOError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def stop_process(wait_thread)
|
|
172
|
+
return unless wait_thread&.alive?
|
|
173
|
+
|
|
174
|
+
kill_process("TERM", wait_thread.pid)
|
|
175
|
+
return if wait_thread.join(1)
|
|
176
|
+
|
|
177
|
+
kill_process("KILL", wait_thread.pid)
|
|
178
|
+
wait_thread.join
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def kill_process(signal, pid)
|
|
182
|
+
::Process.kill(signal, pid)
|
|
183
|
+
rescue Errno::ESRCH
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def join_readers(readers)
|
|
188
|
+
Array(readers).each(&:join)
|
|
189
|
+
end
|
|
133
190
|
end
|
|
134
191
|
|
|
135
192
|
# @overload sh(*args, &block)
|
|
136
193
|
# Runs a command and captures stdout, stderr, and exit status.
|
|
137
194
|
# @param args [Array<String>] command and arguments
|
|
138
|
-
# @yield optional
|
|
139
|
-
# @yieldreturn [String, nil]
|
|
195
|
+
# @yield optional stdin producer, evaluated once after output readers start
|
|
196
|
+
# @yieldreturn [String, nil] content to write to stdin; nil writes nothing
|
|
140
197
|
# @return [Sevgi::Function::Shell::Result]
|
|
141
|
-
# @raise [
|
|
198
|
+
# @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
|
|
199
|
+
# @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
|
|
200
|
+
# @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
|
|
201
|
+
# child process and the second SIGINT sends KILL; the previous SIGINT handler is restored before return.
|
|
142
202
|
def sh(...) = Runner.new.(...)
|
|
143
203
|
|
|
144
204
|
# Runs a command, requiring both executable lookup and successful exit status.
|
|
145
205
|
# @param args [Array<String>] command and arguments
|
|
146
|
-
# @yield optional
|
|
147
|
-
# @yieldreturn [String, nil]
|
|
206
|
+
# @yield optional stdin producer, evaluated once after output readers start
|
|
207
|
+
# @yieldreturn [String, nil] content to write to stdin; nil writes nothing
|
|
148
208
|
# @return [Sevgi::Function::Shell::Result]
|
|
149
209
|
# @raise [Sevgi::Error] when the executable is missing or the command fails
|
|
150
|
-
# @raise [
|
|
210
|
+
# @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
|
|
211
|
+
# @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
|
|
212
|
+
# @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
|
|
213
|
+
# child process and the second SIGINT sends KILL; the previous SIGINT handler is restored before return.
|
|
151
214
|
def sh!(*args, &block)
|
|
152
215
|
executable!(*args) unless args.empty?
|
|
153
216
|
|
|
@@ -163,7 +226,13 @@ module Sevgi
|
|
|
163
226
|
|
|
164
227
|
private
|
|
165
228
|
|
|
166
|
-
def
|
|
229
|
+
def executable_file?(path)
|
|
230
|
+
::File.file?(path) && ::File.executable?(path)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def slash_path?(program)
|
|
234
|
+
program.include?(::File::SEPARATOR) || (::File::ALT_SEPARATOR && program.include?(::File::ALT_SEPARATOR))
|
|
235
|
+
end
|
|
167
236
|
end
|
|
168
237
|
|
|
169
238
|
extend Shell
|
|
@@ -22,19 +22,17 @@ module Sevgi
|
|
|
22
22
|
# Lightweight English pluralization helper.
|
|
23
23
|
module Pluralize
|
|
24
24
|
# Words that should not be pluralized.
|
|
25
|
-
UNCOUNTABLES =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
information
|
|
35
|
-
equipment
|
|
36
|
-
].map { [it, true] }.flatten
|
|
25
|
+
UNCOUNTABLES = %w[
|
|
26
|
+
equipment
|
|
27
|
+
fish
|
|
28
|
+
information
|
|
29
|
+
money
|
|
30
|
+
rice
|
|
31
|
+
series
|
|
32
|
+
sheep
|
|
33
|
+
species
|
|
37
34
|
]
|
|
35
|
+
.to_h { [it, true] }
|
|
38
36
|
.freeze
|
|
39
37
|
|
|
40
38
|
# Singular-to-plural forms that do not follow suffix rules.
|
|
@@ -99,7 +97,7 @@ module Sevgi
|
|
|
99
97
|
def pluralize(word)
|
|
100
98
|
result = word.to_s.dup
|
|
101
99
|
|
|
102
|
-
return result if
|
|
100
|
+
return result if result.empty? || UNCOUNTABLES.key?(result) || PLURALS.key?(result)
|
|
103
101
|
return IRREGULARS[result] if IRREGULARS.key?(result)
|
|
104
102
|
|
|
105
103
|
RULES.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-function
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.94.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -15,6 +15,8 @@ executables: []
|
|
|
15
15
|
extensions: []
|
|
16
16
|
extra_rdoc_files: []
|
|
17
17
|
files:
|
|
18
|
+
- CHANGELOG.md
|
|
19
|
+
- LICENSE
|
|
18
20
|
- README.md
|
|
19
21
|
- lib/sevgi/core.rb
|
|
20
22
|
- lib/sevgi/function.rb
|
|
@@ -41,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
41
43
|
requirements:
|
|
42
44
|
- - ">="
|
|
43
45
|
- !ruby/object:Gem::Version
|
|
44
|
-
version: 3.4.0
|
|
46
|
+
version: 3.4.0
|
|
45
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
48
|
requirements:
|
|
47
49
|
- - ">="
|