hanami-utils 1.0.4 → 1.1.0.beta1
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 +8 -9
- data/README.md +4 -0
- data/hanami-utils.gemspec +2 -1
- data/lib/hanami/interactor.rb +128 -22
- data/lib/hanami/logger.rb +90 -12
- data/lib/hanami/utils/basic_object.rb +3 -4
- data/lib/hanami/utils/files.rb +384 -0
- data/lib/hanami/utils/hash.rb +7 -0
- data/lib/hanami/utils/inflector.rb +2 -6
- data/lib/hanami/utils/string.rb +311 -1
- data/lib/hanami/utils/version.rb +1 -1
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe0165282131db30a45dc60b913b4a36f9eb2095
|
4
|
+
data.tar.gz: c5ec0be6be92bd58c1e0b6043634b0d0af68e7fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4eaf7e264153e4062cafa3eccead00a4793e64e4c44f291c4b573819b4f480155122198e37d3a13283e0ad1e01a0cf96e0e3e5e3c7de3ddd48d64a35bfd7e07
|
7
|
+
data.tar.gz: 93ca73d54081fb22c927a398dfd6e7d4573554a48d8847a7aef91b6134bbea4904b122a8fd577e4f0fac77420e3c9df2a0c3176e019a572b5923f9ace3718dff
|
data/CHANGELOG.md
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
# Hanami::Utils
|
2
2
|
Ruby core extentions and class utilities for Hanami
|
3
3
|
|
4
|
-
## v1.0.
|
5
|
-
###
|
6
|
-
- [
|
7
|
-
- [
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
- [
|
12
|
-
- [Xavier Barbosa] Fix pluralization/singularization for `"area" => "areas"`
|
4
|
+
## v1.1.0.beta1 - 2017-08-11
|
5
|
+
### Added
|
6
|
+
- [Marion Duprey] Allow `Hanami::Interactor#call` to accept arguments. `#initialize` should be used for Dependency Injection, while `#call` should be used for input
|
7
|
+
- [Marion Schleifer] Introduce `Utils::Hash.stringify`
|
8
|
+
- [Marion Schleifer] Introduce `Utils::String.titleize`, `.capitalize`, `.classify`, `.underscore`, `.dasherize`, `.demodulize`, `.namespace`, `.pluralize`, `.singularize`, and `.rsub`
|
9
|
+
- [Luca Guidi] Introduce `Utils::Files`: a set of utils for file manipulations
|
10
|
+
- [Luca Guidi] Introduce `Utils::String.transform` a pipelined transformations for strings
|
11
|
+
- [Marion Duprey & Gabriel Gizotti] Filter sensitive informations for `Hanami::Logger`
|
13
12
|
|
14
13
|
## v1.0.2 - 2017-07-10
|
15
14
|
### Fixed
|
data/README.md
CHANGED
@@ -92,6 +92,10 @@ Safe and fast escape for URLs, HTML content and attributes. Based on OWASP/ESAPI
|
|
92
92
|
|
93
93
|
Recursive, cross-platform ordered list of files. [[API doc](http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/FileList)]
|
94
94
|
|
95
|
+
### Hanami::Utils::Files
|
96
|
+
|
97
|
+
File utilities to manipulate files and directories. [[API doc](http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/Files)]
|
98
|
+
|
95
99
|
### Hanami::Utils::Hash
|
96
100
|
|
97
101
|
Enhanced version of Ruby's `Hash`. [[API doc](http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/Hash)]
|
data/hanami-utils.gemspec
CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.require_paths = ['lib']
|
19
19
|
spec.required_ruby_version = '>= 2.3.0'
|
20
20
|
|
21
|
-
spec.add_dependency 'transproc',
|
21
|
+
spec.add_dependency 'transproc', '~> 1.0'
|
22
|
+
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
22
23
|
|
23
24
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
25
|
spec.add_development_dependency 'rake', '~> 11'
|
data/lib/hanami/interactor.rb
CHANGED
@@ -145,15 +145,14 @@ module Hanami
|
|
145
145
|
super
|
146
146
|
|
147
147
|
base.class_eval do
|
148
|
-
|
149
|
-
extend ClassMethods
|
148
|
+
extend ClassMethods
|
150
149
|
end
|
151
150
|
end
|
152
151
|
|
153
|
-
# Interactor interface
|
152
|
+
# Interactor legacy interface
|
154
153
|
#
|
155
154
|
# @since 0.3.5
|
156
|
-
module
|
155
|
+
module LegacyInterface
|
157
156
|
# Initialize an interactor
|
158
157
|
#
|
159
158
|
# It accepts arbitrary number of arguments.
|
@@ -262,6 +261,118 @@ module Hanami
|
|
262
261
|
def call
|
263
262
|
_call { super }
|
264
263
|
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
# @since 0.3.5
|
268
|
+
# @api private
|
269
|
+
def _call
|
270
|
+
catch :fail do
|
271
|
+
validate!
|
272
|
+
yield
|
273
|
+
end
|
274
|
+
|
275
|
+
_prepare!
|
276
|
+
end
|
277
|
+
|
278
|
+
# @since 0.3.5
|
279
|
+
def validate!
|
280
|
+
fail! unless valid?
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Interactor interface
|
285
|
+
# @since 1.1.0
|
286
|
+
module Interface
|
287
|
+
# Triggers the operation and return a result.
|
288
|
+
#
|
289
|
+
# All the exposed instance variables will be available in the result.
|
290
|
+
#
|
291
|
+
# ATTENTION: This must be implemented by the including class.
|
292
|
+
#
|
293
|
+
# @return [Hanami::Interactor::Result] the result of the operation
|
294
|
+
#
|
295
|
+
# @raise [NoMethodError] if this isn't implemented by the including class.
|
296
|
+
#
|
297
|
+
# @example Expose instance variables in result payload
|
298
|
+
# require 'hanami/interactor'
|
299
|
+
#
|
300
|
+
# class Signup
|
301
|
+
# include Hanami::Interactor
|
302
|
+
# expose :user, :params
|
303
|
+
#
|
304
|
+
# def call(params)
|
305
|
+
# @params = params
|
306
|
+
# @foo = 'bar'
|
307
|
+
# @user = UserRepository.new.persist(User.new(params))
|
308
|
+
# end
|
309
|
+
# end
|
310
|
+
#
|
311
|
+
# result = Signup.new(name: 'Luca').call
|
312
|
+
# result.failure? # => false
|
313
|
+
# result.successful? # => true
|
314
|
+
#
|
315
|
+
# result.user # => #<User:0x007fa311105778 @id=1 @name="Luca">
|
316
|
+
# result.params # => { :name=>"Luca" }
|
317
|
+
# result.foo # => raises NoMethodError
|
318
|
+
#
|
319
|
+
# @example Failed precondition
|
320
|
+
# require 'hanami/interactor'
|
321
|
+
#
|
322
|
+
# class Signup
|
323
|
+
# include Hanami::Interactor
|
324
|
+
# expose :user
|
325
|
+
#
|
326
|
+
# # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false
|
327
|
+
# def call(params)
|
328
|
+
# @user = User.new(params)
|
329
|
+
# @user = UserRepository.new.persist(@user)
|
330
|
+
# end
|
331
|
+
#
|
332
|
+
# private
|
333
|
+
# def valid?(params)
|
334
|
+
# params.valid?
|
335
|
+
# end
|
336
|
+
# end
|
337
|
+
#
|
338
|
+
# result = Signup.new.call(name: nil)
|
339
|
+
# result.successful? # => false
|
340
|
+
# result.failure? # => true
|
341
|
+
#
|
342
|
+
# result.user # => nil
|
343
|
+
#
|
344
|
+
# @example Bad usage
|
345
|
+
# require 'hanami/interactor'
|
346
|
+
#
|
347
|
+
# class Signup
|
348
|
+
# include Hanami::Interactor
|
349
|
+
#
|
350
|
+
# # Method #call is not defined
|
351
|
+
# end
|
352
|
+
#
|
353
|
+
# Signup.new.call # => NoMethodError
|
354
|
+
def call(*args, **kwargs)
|
355
|
+
@__result = ::Hanami::Interactor::Result.new
|
356
|
+
_call(*args, **kwargs) { super }
|
357
|
+
end
|
358
|
+
|
359
|
+
private
|
360
|
+
|
361
|
+
# @api private
|
362
|
+
# @since 1.1.0
|
363
|
+
def _call(*args, **kwargs)
|
364
|
+
catch :fail do
|
365
|
+
validate!(*args, **kwargs)
|
366
|
+
yield
|
367
|
+
end
|
368
|
+
|
369
|
+
_prepare!
|
370
|
+
end
|
371
|
+
|
372
|
+
# @since 1.1.0
|
373
|
+
def validate!(*args, **kwargs)
|
374
|
+
fail! unless valid?(*args, **kwargs)
|
375
|
+
end
|
265
376
|
end
|
266
377
|
|
267
378
|
private
|
@@ -274,7 +385,7 @@ module Hanami
|
|
274
385
|
# @return [TrueClass,FalseClass] the result of the check
|
275
386
|
#
|
276
387
|
# @since 0.3.5
|
277
|
-
def valid?
|
388
|
+
def valid?(*)
|
278
389
|
true
|
279
390
|
end
|
280
391
|
|
@@ -426,22 +537,6 @@ module Hanami
|
|
426
537
|
fail!
|
427
538
|
end
|
428
539
|
|
429
|
-
# @since 0.3.5
|
430
|
-
# @api private
|
431
|
-
def _call
|
432
|
-
catch :fail do
|
433
|
-
validate!
|
434
|
-
yield
|
435
|
-
end
|
436
|
-
|
437
|
-
_prepare!
|
438
|
-
end
|
439
|
-
|
440
|
-
# @since 0.3.5
|
441
|
-
def validate!
|
442
|
-
fail! unless valid?
|
443
|
-
end
|
444
|
-
|
445
540
|
# @since 0.3.5
|
446
541
|
# @api private
|
447
542
|
def _prepare!
|
@@ -453,7 +548,7 @@ module Hanami
|
|
453
548
|
def _exposures
|
454
549
|
Hash[].tap do |result|
|
455
550
|
self.class.exposures.each do |name, ivar|
|
456
|
-
result[name] = instance_variable_get(ivar)
|
551
|
+
result[name] = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
|
457
552
|
end
|
458
553
|
end
|
459
554
|
end
|
@@ -473,6 +568,17 @@ module Hanami
|
|
473
568
|
end
|
474
569
|
end
|
475
570
|
|
571
|
+
def method_added(method_name)
|
572
|
+
super
|
573
|
+
return unless method_name == :call
|
574
|
+
|
575
|
+
if instance_method(:call).arity.zero?
|
576
|
+
prepend Hanami::Interactor::LegacyInterface
|
577
|
+
else
|
578
|
+
prepend Hanami::Interactor::Interface
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
476
582
|
# Expose local instance variables into the returning value of <tt>#call</tt>
|
477
583
|
#
|
478
584
|
# @param instance_variable_names [Symbol,Array<Symbol>] one or more instance
|
data/lib/hanami/logger.rb
CHANGED
@@ -3,6 +3,7 @@ require 'json'
|
|
3
3
|
require 'logger'
|
4
4
|
require 'hanami/utils/string'
|
5
5
|
require 'hanami/utils/json'
|
6
|
+
require 'hanami/utils/hash'
|
6
7
|
require 'hanami/utils/class_attribute'
|
7
8
|
|
8
9
|
module Hanami
|
@@ -117,15 +118,13 @@ module Hanami
|
|
117
118
|
class_attribute :subclasses
|
118
119
|
self.subclasses = Set.new
|
119
120
|
|
120
|
-
def self.fabricate(formatter, application_name)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
formatter
|
128
|
-
end.tap { |f| f.application_name = application_name }
|
121
|
+
def self.fabricate(formatter, application_name, filters)
|
122
|
+
fabricated_formatter = _formatter_instance(formatter)
|
123
|
+
|
124
|
+
fabricated_formatter.application_name = application_name
|
125
|
+
fabricated_formatter.hash_filter = HashFilter.new(filters)
|
126
|
+
|
127
|
+
fabricated_formatter
|
129
128
|
end
|
130
129
|
|
131
130
|
# @api private
|
@@ -139,6 +138,20 @@ module Hanami
|
|
139
138
|
name == :default
|
140
139
|
end
|
141
140
|
|
141
|
+
# @api private
|
142
|
+
# @since 1.1.0
|
143
|
+
def self._formatter_instance(formatter)
|
144
|
+
case formatter
|
145
|
+
when Symbol
|
146
|
+
(subclasses.find { |s| s.eligible?(formatter) } || self).new
|
147
|
+
when nil
|
148
|
+
new
|
149
|
+
else
|
150
|
+
formatter
|
151
|
+
end
|
152
|
+
end
|
153
|
+
private_class_method :_formatter_instance
|
154
|
+
|
142
155
|
# @since 0.5.0
|
143
156
|
# @api private
|
144
157
|
attr_writer :application_name
|
@@ -147,6 +160,10 @@ module Hanami
|
|
147
160
|
# @api private
|
148
161
|
attr_reader :application_name
|
149
162
|
|
163
|
+
# @since 1.1.0
|
164
|
+
# @api private
|
165
|
+
attr_writer :hash_filter
|
166
|
+
|
150
167
|
# @since 0.5.0
|
151
168
|
# @api private
|
152
169
|
#
|
@@ -168,7 +185,7 @@ module Hanami
|
|
168
185
|
def _message_hash(message) # rubocop:disable Metrics/MethodLength
|
169
186
|
case message
|
170
187
|
when Hash
|
171
|
-
message
|
188
|
+
@hash_filter.filter(message)
|
172
189
|
when Exception
|
173
190
|
Hash[
|
174
191
|
message: message.message,
|
@@ -206,6 +223,67 @@ module Hanami
|
|
206
223
|
|
207
224
|
result
|
208
225
|
end
|
226
|
+
|
227
|
+
# Filtering logic
|
228
|
+
#
|
229
|
+
# @since 1.1.0
|
230
|
+
# @api private
|
231
|
+
class HashFilter
|
232
|
+
# @since 1.1.0
|
233
|
+
# @api private
|
234
|
+
attr_reader :filters
|
235
|
+
|
236
|
+
# @since 1.1.0
|
237
|
+
# @api private
|
238
|
+
def initialize(filters = [])
|
239
|
+
@filters = filters
|
240
|
+
end
|
241
|
+
|
242
|
+
# @since 1.1.0
|
243
|
+
# @api private
|
244
|
+
def filter(hash)
|
245
|
+
_filtered_keys(hash).each do |key|
|
246
|
+
*keys, last = _actual_keys(hash, key.split('.'))
|
247
|
+
keys.inject(hash, :fetch)[last] = '[FILTERED]'
|
248
|
+
end
|
249
|
+
|
250
|
+
hash
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
# @since 1.1.0
|
256
|
+
# @api private
|
257
|
+
def _filtered_keys(hash)
|
258
|
+
_key_paths(hash).select { |key| filters.any? { |filter| key =~ %r{(\.|\A)#{filter}(\.|\z)} } }
|
259
|
+
end
|
260
|
+
|
261
|
+
# @since 1.1.0
|
262
|
+
# @api private
|
263
|
+
def _key_paths(hash, base = nil)
|
264
|
+
hash.inject([]) do |results, (k, v)|
|
265
|
+
results + (v.respond_to?(:each) ? _key_paths(v, _build_path(base, k)) : [_build_path(base, k)])
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# @since 1.1.0
|
270
|
+
# @api private
|
271
|
+
def _build_path(base, key)
|
272
|
+
[base, key.to_s].compact.join('.')
|
273
|
+
end
|
274
|
+
|
275
|
+
# @since 1.1.0
|
276
|
+
# @api private
|
277
|
+
def _actual_keys(hash, keys)
|
278
|
+
search_in = hash
|
279
|
+
|
280
|
+
keys.inject([]) do |res, key|
|
281
|
+
correct_key = search_in.key?(key.to_sym) ? key.to_sym : key
|
282
|
+
search_in = search_in[correct_key]
|
283
|
+
res + [correct_key]
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
209
287
|
end
|
210
288
|
|
211
289
|
# Hanami::Logger JSON formatter.
|
@@ -375,13 +453,13 @@ module Hanami
|
|
375
453
|
# logger.info "Hello World"
|
376
454
|
#
|
377
455
|
# # => {"app":"Hanami","severity":"DEBUG","time":"2017-03-30T13:57:59Z","message":"Hello World"}
|
378
|
-
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil)
|
456
|
+
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil, filter: []) # rubocop:disable Metrics/ParameterLists
|
379
457
|
super(stream, *args)
|
380
458
|
|
381
459
|
@level = _level(level)
|
382
460
|
@stream = stream
|
383
461
|
@application_name = application_name
|
384
|
-
@formatter = Formatter.fabricate(formatter, self.application_name)
|
462
|
+
@formatter = Formatter.fabricate(formatter, self.application_name, filter)
|
385
463
|
end
|
386
464
|
|
387
465
|
# Returns the current application name, this is used for tagging purposes
|
@@ -21,7 +21,7 @@ module Hanami
|
|
21
21
|
#
|
22
22
|
# @see http://ruby-doc.org/core/Object.html#method-i-inspect
|
23
23
|
def inspect
|
24
|
-
"#<#{self.class}:#{'
|
24
|
+
"#<#{self.class}:#{'%x' % (__id__ << 1)}#{__inspect}>" # rubocop:disable Style/FormatString
|
25
25
|
end
|
26
26
|
|
27
27
|
# Alias for __id__
|
@@ -37,14 +37,13 @@ module Hanami
|
|
37
37
|
|
38
38
|
# Interface for pp
|
39
39
|
#
|
40
|
-
# @param printer [PP] the Pretty Printable printer
|
41
40
|
# @return [String] the pretty-printable inspection of the object
|
42
41
|
#
|
43
42
|
# @since 0.9.0
|
44
43
|
#
|
45
44
|
# @see https://ruby-doc.org/stdlib/libdoc/pp/rdoc/PP.html
|
46
|
-
def pretty_print(
|
47
|
-
|
45
|
+
def pretty_print(*)
|
46
|
+
inspect
|
48
47
|
end
|
49
48
|
|
50
49
|
# Returns true if responds to the given method.
|
@@ -0,0 +1,384 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
module Hanami
|
5
|
+
module Utils
|
6
|
+
# Files utilities
|
7
|
+
#
|
8
|
+
# @since 1.1.0
|
9
|
+
module Files # rubocop:disable Metrics/ModuleLength
|
10
|
+
# Creates an empty file for the given path.
|
11
|
+
# All the intermediate directories are created.
|
12
|
+
# If the path already exists, it doesn't change the contents
|
13
|
+
#
|
14
|
+
# @param path [String,Pathname] the path to file
|
15
|
+
#
|
16
|
+
# @since 1.1.0
|
17
|
+
def self.touch(path)
|
18
|
+
write(path, "")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates a new file for the given path and content.
|
22
|
+
# All the intermediate directories are created.
|
23
|
+
# If the path already exists, it appends the contents.
|
24
|
+
#
|
25
|
+
# @param path [String,Pathname] the path to file
|
26
|
+
# @param content [String, Array<String>] the content to write
|
27
|
+
#
|
28
|
+
# @since 1.1.0
|
29
|
+
def self.write(path, *content)
|
30
|
+
mkdir_p(path)
|
31
|
+
open(path, ::File::CREAT | ::File::WRONLY, *content)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Rewrites the contents of an existing file.
|
35
|
+
# If the path already exists, it replaces the contents.
|
36
|
+
#
|
37
|
+
# @param path [String,Pathname] the path to file
|
38
|
+
# @param content [String, Array<String>] the content to write
|
39
|
+
#
|
40
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
41
|
+
#
|
42
|
+
# @since 1.1.0
|
43
|
+
def self.rewrite(path, *content)
|
44
|
+
open(path, ::File::TRUNC | ::File::WRONLY, *content)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Copies source into destination.
|
48
|
+
# All the intermediate directories are created.
|
49
|
+
# If the destination already exists, it overrides the contents.
|
50
|
+
#
|
51
|
+
# @param source [String,Pathname] the path to the source file
|
52
|
+
# @param destination [String,Pathname] the path to the destination file
|
53
|
+
#
|
54
|
+
# @since 1.1.0
|
55
|
+
def self.cp(source, destination)
|
56
|
+
mkdir_p(destination)
|
57
|
+
FileUtils.cp(source, destination)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a directory for the given path.
|
61
|
+
# It assumes that all the tokens in `path` are meant to be a directory.
|
62
|
+
# All the intermediate directories are created.
|
63
|
+
#
|
64
|
+
# @param path [String,Pathname] the path to directory
|
65
|
+
#
|
66
|
+
# @since 1.1.0
|
67
|
+
#
|
68
|
+
# @see .mkdir_p
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# require "hanami/utils/files"
|
72
|
+
#
|
73
|
+
# Hanami::Utils::Files.mkdir("path/to/directory")
|
74
|
+
# # => creates the `path/to/directory` directory
|
75
|
+
#
|
76
|
+
# # WRONG this isn't probably what you want, check `.mkdir_p`
|
77
|
+
# Hanami::Utils::Files.mkdir("path/to/file.rb")
|
78
|
+
# # => creates the `path/to/file.rb` directory
|
79
|
+
def self.mkdir(path)
|
80
|
+
FileUtils.mkdir_p(path)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates a directory for the given path.
|
84
|
+
# It assumes that all the tokens, but the last, in `path` are meant to be
|
85
|
+
# a directory, whereas the last is meant to be a file.
|
86
|
+
# All the intermediate directories are created.
|
87
|
+
#
|
88
|
+
# @param path [String,Pathname] the path to directory
|
89
|
+
#
|
90
|
+
# @since 1.1.0
|
91
|
+
#
|
92
|
+
# @see .mkdir
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# require "hanami/utils/files"
|
96
|
+
#
|
97
|
+
# Hanami::Utils::Files.mkdir_p("path/to/file.rb")
|
98
|
+
# # => creates the `path/to` directory, but NOT `file.rb`
|
99
|
+
#
|
100
|
+
# # WRONG it doesn't create the last directory, check `.mkdir`
|
101
|
+
# Hanami::Utils::Files.mkdir_p("path/to/directory")
|
102
|
+
# # => creates the `path/to` directory
|
103
|
+
def self.mkdir_p(path)
|
104
|
+
Pathname.new(path).dirname.mkpath
|
105
|
+
end
|
106
|
+
|
107
|
+
# Deletes given path (file).
|
108
|
+
#
|
109
|
+
# @param path [String,Pathname] the path to file
|
110
|
+
#
|
111
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
112
|
+
#
|
113
|
+
# @since 1.1.0
|
114
|
+
def self.delete(path)
|
115
|
+
FileUtils.rm(path)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Deletes given path (directory).
|
119
|
+
#
|
120
|
+
# @param path [String,Pathname] the path to file
|
121
|
+
#
|
122
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
123
|
+
#
|
124
|
+
# @since 1.1.0
|
125
|
+
def self.delete_directory(path)
|
126
|
+
FileUtils.remove_entry_secure(path)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Adds a new line at the top of the file
|
130
|
+
#
|
131
|
+
# @param path [String,Pathname] the path to file
|
132
|
+
# @param line [String] the line to add
|
133
|
+
#
|
134
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
135
|
+
#
|
136
|
+
# @see .append
|
137
|
+
#
|
138
|
+
# @since 1.1.0
|
139
|
+
def self.unshift(path, line)
|
140
|
+
content = ::File.readlines(path)
|
141
|
+
content.unshift("#{line}\n")
|
142
|
+
|
143
|
+
rewrite(path, content)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Adds a new line at the bottom of the file
|
147
|
+
#
|
148
|
+
# @param path [String,Pathname] the path to file
|
149
|
+
# @param contents [String] the contents to add
|
150
|
+
#
|
151
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
152
|
+
#
|
153
|
+
# @see .unshift
|
154
|
+
#
|
155
|
+
# @since 1.1.0
|
156
|
+
def self.append(path, contents)
|
157
|
+
mkdir_p(path)
|
158
|
+
|
159
|
+
content = ::File.readlines(path)
|
160
|
+
content << "#{contents}\n"
|
161
|
+
|
162
|
+
rewrite(path, content)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Replace first line in `path` that contains `target` with `replacement`.
|
166
|
+
#
|
167
|
+
# @param path [String,Pathname] the path to file
|
168
|
+
# @param target [String,Regexp] the target to replace
|
169
|
+
# @param replacement [String] the replacement
|
170
|
+
#
|
171
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
172
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
173
|
+
#
|
174
|
+
# @see .replace_last_line
|
175
|
+
#
|
176
|
+
# @since 1.1.0
|
177
|
+
def self.replace_first_line(path, target, replacement)
|
178
|
+
content = ::File.readlines(path)
|
179
|
+
content[index(content, path, target)] = "#{replacement}\n"
|
180
|
+
|
181
|
+
rewrite(path, content)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Replace last line in `path` that contains `target` with `replacement`.
|
185
|
+
#
|
186
|
+
# @param path [String,Pathname] the path to file
|
187
|
+
# @param target [String,Regexp] the target to replace
|
188
|
+
# @param replacement [String] the replacement
|
189
|
+
#
|
190
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
191
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
192
|
+
#
|
193
|
+
# @see .replace_first_line
|
194
|
+
#
|
195
|
+
# @since 1.1.0
|
196
|
+
def self.replace_last_line(path, target, replacement)
|
197
|
+
content = ::File.readlines(path)
|
198
|
+
content[-index(content.reverse, path, target) - 1] = "#{replacement}\n"
|
199
|
+
|
200
|
+
rewrite(path, content)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Inject `contents` in `path` before `target`.
|
204
|
+
#
|
205
|
+
# @param path [String,Pathname] the path to file
|
206
|
+
# @param target [String,Regexp] the target to replace
|
207
|
+
# @param contents [String] the contents to inject
|
208
|
+
#
|
209
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
210
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
211
|
+
#
|
212
|
+
# @see .inject_line_after
|
213
|
+
#
|
214
|
+
# @since 1.1.0
|
215
|
+
def self.inject_line_before(path, target, contents)
|
216
|
+
content = ::File.readlines(path)
|
217
|
+
i = index(content, path, target)
|
218
|
+
|
219
|
+
content.insert(i, "#{contents}\n")
|
220
|
+
rewrite(path, content)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Inject `contents` in `path` after `target`.
|
224
|
+
#
|
225
|
+
# @param path [String,Pathname] the path to file
|
226
|
+
# @param target [String,Regexp] the target to replace
|
227
|
+
# @param contents [String] the contents to inject
|
228
|
+
#
|
229
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
230
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
231
|
+
#
|
232
|
+
# @see .inject_line_before
|
233
|
+
#
|
234
|
+
# @since 1.1.0
|
235
|
+
def self.inject_line_after(path, target, contents)
|
236
|
+
content = ::File.readlines(path)
|
237
|
+
i = index(content, path, target)
|
238
|
+
|
239
|
+
content.insert(i + 1, "#{contents}\n")
|
240
|
+
rewrite(path, content)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Removes line from `path`, matching `target`.
|
244
|
+
#
|
245
|
+
# @param path [String,Pathname] the path to file
|
246
|
+
# @param target [String,Regexp] the target to remove
|
247
|
+
#
|
248
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
249
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
250
|
+
#
|
251
|
+
# @since 1.1.0
|
252
|
+
def self.remove_line(path, target)
|
253
|
+
content = ::File.readlines(path)
|
254
|
+
i = index(content, path, target)
|
255
|
+
|
256
|
+
content.delete_at(i)
|
257
|
+
rewrite(path, content)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Removes `target` block from `path`
|
261
|
+
#
|
262
|
+
# @param path [String,Pathname] the path to file
|
263
|
+
# @param target [String] the target block to remove
|
264
|
+
#
|
265
|
+
# @raise [Errno::ENOENT] if the path doesn't exist
|
266
|
+
# @raise [ArgumentError] if `target` cannot be found in `path`
|
267
|
+
#
|
268
|
+
# @since 1.1.0
|
269
|
+
#
|
270
|
+
# @example
|
271
|
+
# require "hanami/utils/files"
|
272
|
+
#
|
273
|
+
# puts File.read("app.rb")
|
274
|
+
#
|
275
|
+
# # class App
|
276
|
+
# # configure do
|
277
|
+
# # root __dir__
|
278
|
+
# # end
|
279
|
+
# # end
|
280
|
+
#
|
281
|
+
# Hanami::Utils::Files.remove_block("app.rb", "configure")
|
282
|
+
#
|
283
|
+
# puts File.read("app.rb")
|
284
|
+
#
|
285
|
+
# # class App
|
286
|
+
# # end
|
287
|
+
def self.remove_block(path, target) # rubocop:disable Metrics/AbcSize
|
288
|
+
content = ::File.readlines(path)
|
289
|
+
starting = index(content, path, target)
|
290
|
+
line = content[starting]
|
291
|
+
size = line[/\A[[:space:]]*/].bytesize
|
292
|
+
closing = (" " * size) + (target =~ /{/ ? '}' : 'end')
|
293
|
+
ending = starting + index(content[starting..-1], path, closing)
|
294
|
+
|
295
|
+
content.slice!(starting..ending)
|
296
|
+
rewrite(path, content)
|
297
|
+
|
298
|
+
remove_block(path, target) if match?(content, target)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Checks if `path` exist
|
302
|
+
#
|
303
|
+
# @param path [String,Pathname] the path to file
|
304
|
+
#
|
305
|
+
# @return [TrueClass,FalseClass] the result of the check
|
306
|
+
#
|
307
|
+
# @since 1.1.0
|
308
|
+
#
|
309
|
+
# @example
|
310
|
+
# require "hanami/utils/files"
|
311
|
+
#
|
312
|
+
# Hanami::Utils::Files.exist?(__FILE__) # => true
|
313
|
+
# Hanami::Utils::Files.exist?(__dir__) # => true
|
314
|
+
#
|
315
|
+
# Hanami::Utils::Files.exist?("missing_file") # => false
|
316
|
+
def self.exist?(path)
|
317
|
+
File.exist?(path)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Checks if `path` is a directory
|
321
|
+
#
|
322
|
+
# @param path [String,Pathname] the path to directory
|
323
|
+
#
|
324
|
+
# @return [TrueClass,FalseClass] the result of the check
|
325
|
+
#
|
326
|
+
# @since 1.1.0
|
327
|
+
#
|
328
|
+
# @example
|
329
|
+
# require "hanami/utils/files"
|
330
|
+
#
|
331
|
+
# Hanami::Utils::Files.directory?(__dir__) # => true
|
332
|
+
# Hanami::Utils::Files.directory?(__FILE__) # => false
|
333
|
+
#
|
334
|
+
# Hanami::Utils::Files.directory?("missing_directory") # => false
|
335
|
+
def self.directory?(path)
|
336
|
+
File.directory?(path)
|
337
|
+
end
|
338
|
+
|
339
|
+
# private
|
340
|
+
|
341
|
+
# @since 1.1.0
|
342
|
+
# @api private
|
343
|
+
def self.match?(content, target)
|
344
|
+
!line_number(content, target).nil?
|
345
|
+
end
|
346
|
+
|
347
|
+
private_class_method :match?
|
348
|
+
|
349
|
+
# @since 1.1.0
|
350
|
+
# @api private
|
351
|
+
def self.open(path, mode, *content)
|
352
|
+
::File.open(path, mode) do |file|
|
353
|
+
file.write(Array(content).flatten.join)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
private_class_method :open
|
358
|
+
|
359
|
+
# @since 1.1.0
|
360
|
+
# @api private
|
361
|
+
def self.index(content, path, target)
|
362
|
+
line_number(content, target) or
|
363
|
+
raise ArgumentError.new("Cannot find `#{target}' inside `#{path}'.")
|
364
|
+
end
|
365
|
+
|
366
|
+
private_class_method :index
|
367
|
+
|
368
|
+
# @since 1.1.0
|
369
|
+
# @api private
|
370
|
+
def self.line_number(content, target)
|
371
|
+
content.index do |l|
|
372
|
+
case target
|
373
|
+
when ::String
|
374
|
+
l.include?(target)
|
375
|
+
when Regexp
|
376
|
+
l =~ target
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
private_class_method :line_number
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
data/lib/hanami/utils/hash.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# rubocop:disable ClassLength
|
2
|
+
|
1
3
|
require 'hanami/utils/duplicable'
|
2
4
|
require 'transproc'
|
3
5
|
|
@@ -206,6 +208,7 @@ module Hanami
|
|
206
208
|
#
|
207
209
|
# hash.keys # => [:a, :b]
|
208
210
|
# hash.inspect # => {"a"=>23, "b"=>{"c"=>["x", "y", "z"]}}
|
211
|
+
|
209
212
|
def stringify!
|
210
213
|
keys.each do |k|
|
211
214
|
v = delete(k)
|
@@ -217,6 +220,10 @@ module Hanami
|
|
217
220
|
self
|
218
221
|
end
|
219
222
|
|
223
|
+
def self.stringify(input)
|
224
|
+
self[:stringify_keys].call(input)
|
225
|
+
end
|
226
|
+
|
220
227
|
# Return a deep copy of the current Hanami::Utils::Hash
|
221
228
|
#
|
222
229
|
# @return [Hash] a deep duplicated self
|
@@ -226,8 +226,7 @@ module Hanami
|
|
226
226
|
'police' => 'police',
|
227
227
|
# regressions
|
228
228
|
# https://github.com/hanami/utils/issues/106
|
229
|
-
'album' => 'albums'
|
230
|
-
'area' => 'areas'
|
229
|
+
'album' => 'albums'
|
231
230
|
)
|
232
231
|
|
233
232
|
# Irregular rules for singulars
|
@@ -268,11 +267,8 @@ module Hanami
|
|
268
267
|
'species' => 'species',
|
269
268
|
'police' => 'police',
|
270
269
|
# fallback
|
271
|
-
'areas' => 'area',
|
272
270
|
'hives' => 'hive',
|
273
|
-
'phases' => 'phase'
|
274
|
-
'exercises' => 'exercise',
|
275
|
-
'releases' => 'release'
|
271
|
+
'phases' => 'phase'
|
276
272
|
)
|
277
273
|
|
278
274
|
# Block for custom inflection rules.
|
data/lib/hanami/utils/string.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'hanami/utils/inflector'
|
2
|
+
require 'transproc'
|
3
|
+
require 'concurrent/map'
|
2
4
|
|
3
5
|
module Hanami
|
4
6
|
module Utils
|
@@ -72,6 +74,314 @@ module Hanami
|
|
72
74
|
# @api private
|
73
75
|
CLASSIFY_WORD_SEPARATOR = /#{CLASSIFY_SEPARATOR}|#{NAMESPACE_SEPARATOR}|#{UNDERSCORE_SEPARATOR}|#{DASHERIZE_SEPARATOR}/
|
74
76
|
|
77
|
+
@__transformations__ = Concurrent::Map.new
|
78
|
+
|
79
|
+
extend Transproc::Registry
|
80
|
+
extend Transproc::Composer
|
81
|
+
|
82
|
+
# Apply the given transformation(s) to `input`
|
83
|
+
#
|
84
|
+
# It performs a pipeline of transformations, by applying the given functions from `Hanami::Utils::String` and `::String`.
|
85
|
+
# The transformations are applied in the given order.
|
86
|
+
#
|
87
|
+
# It doesn't mutate the input, unless you use destructive methods from `::String`
|
88
|
+
#
|
89
|
+
# @param input [::String] the string to be transformed
|
90
|
+
# @param transformations [Array<Symbol,Proc,Array>] one or many
|
91
|
+
# transformations expressed as:
|
92
|
+
# * `Symbol` to reference a function from `Hanami::Utils::String` or `String`.
|
93
|
+
# * `Proc` an anonymous function that MUST accept one input
|
94
|
+
# * `Array` where the first element is a `Symbol` to reference a
|
95
|
+
# function from `Hanami::Utils::String` or `String` and the rest of
|
96
|
+
# the elements are the arguments to pass
|
97
|
+
#
|
98
|
+
# @return [::String] the result of the transformations
|
99
|
+
#
|
100
|
+
# @raise [NoMethodError] if a `Hanami::Utils::String` and `::String`
|
101
|
+
# don't respond to a given method name
|
102
|
+
#
|
103
|
+
# @raise [ArgumentError] if a Proc transformation has an arity not equal
|
104
|
+
# to 1
|
105
|
+
#
|
106
|
+
# @since 1.1.0
|
107
|
+
#
|
108
|
+
# @example Basic usage
|
109
|
+
# require "hanami/utils/string"
|
110
|
+
#
|
111
|
+
# Hanami::Utils::String.transform("hanami/utils", :underscore, :classify)
|
112
|
+
# # => "Hanami::Utils"
|
113
|
+
#
|
114
|
+
# Hanami::Utils::String.transform("Hanami::Utils::String", [:gsub, /[aeiouy]/, "*"], :demodulize)
|
115
|
+
# # => "H*n*m*"
|
116
|
+
#
|
117
|
+
# Hanami::Utils::String.transform("Hanami", ->(s) { s.upcase })
|
118
|
+
# # => "HANAMI"
|
119
|
+
#
|
120
|
+
# @example Unkown transformation
|
121
|
+
# require "hanami/utils/string"
|
122
|
+
#
|
123
|
+
# Hanami::Utils::String.transform("Sakura", :foo)
|
124
|
+
# # => NoMethodError: undefined method `:foo' for "Sakura":String
|
125
|
+
#
|
126
|
+
# @example Proc with arity not equal to 1
|
127
|
+
# require "hanami/utils/string"
|
128
|
+
#
|
129
|
+
# Hanami::Utils::String.transform("Cherry", -> { "blossom" }))
|
130
|
+
# # => ArgumentError: wrong number of arguments (given 1, expected 0)
|
131
|
+
#
|
132
|
+
# rubocop:disable Metrics/MethodLength
|
133
|
+
# rubocop:disable Metrics/AbcSize
|
134
|
+
def self.transform(input, *transformations)
|
135
|
+
fn = @__transformations__.fetch_or_store(transformations.hash) do
|
136
|
+
compose do |fns|
|
137
|
+
transformations.each do |transformation, *args|
|
138
|
+
fns << if transformation.is_a?(Proc)
|
139
|
+
transformation
|
140
|
+
elsif contain?(transformation)
|
141
|
+
self[transformation, *args]
|
142
|
+
elsif input.respond_to?(transformation)
|
143
|
+
t(:bind, input, ->(i) { i.public_send(transformation, *args) })
|
144
|
+
else
|
145
|
+
raise NoMethodError.new(%(undefined method `#{transformation.inspect}' for #{input.inspect}:#{input.class}))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
fn.call(input)
|
152
|
+
end
|
153
|
+
# rubocop:enable Metrics/AbcSize
|
154
|
+
# rubocop:enable Metrics/MethodLength
|
155
|
+
|
156
|
+
# Extracted from `transproc` source code
|
157
|
+
#
|
158
|
+
# `transproc` is Copyright 2014 by Piotr Solnica (piotr.solnica@gmail.com),
|
159
|
+
# released under the MIT License
|
160
|
+
#
|
161
|
+
# @since 1.1.0
|
162
|
+
# @api private
|
163
|
+
def self.bind(value, binding, fn)
|
164
|
+
binding.instance_exec(value, &fn)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return a titleized version of the string
|
168
|
+
#
|
169
|
+
# @param input [::String] the input
|
170
|
+
#
|
171
|
+
# @return [::String] the transformed string
|
172
|
+
#
|
173
|
+
# @since 1.1.0
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# require 'hanami/utils/string'
|
177
|
+
#
|
178
|
+
# Hanami::Utils::String.titleize('hanami utils') # => "Hanami Utils"
|
179
|
+
def self.titleize(input)
|
180
|
+
string = ::String.new(input.to_s)
|
181
|
+
underscore(string).split(CLASSIFY_SEPARATOR).map(&:capitalize).join(TITLEIZE_SEPARATOR)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Return a capitalized version of the string
|
185
|
+
#
|
186
|
+
# @param input [::String] the input
|
187
|
+
#
|
188
|
+
# @return [::String] the transformed string
|
189
|
+
#
|
190
|
+
# @since 1.1.0
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# require 'hanami/utils/string'
|
194
|
+
#
|
195
|
+
# Hanami::Utils::String.capitalize('hanami') # => "Hanami"
|
196
|
+
#
|
197
|
+
# Hanami::Utils::String.capitalize('hanami utils') # => "Hanami utils"
|
198
|
+
#
|
199
|
+
# Hanami::Utils::String.capitalize('Hanami Utils') # => "Hanami utils"
|
200
|
+
#
|
201
|
+
# Hanami::Utils::String.capitalize('hanami_utils') # => "Hanami utils"
|
202
|
+
#
|
203
|
+
# Hanami::Utils::String.capitalize('hanami-utils') # => "Hanami utils"
|
204
|
+
def self.capitalize(input)
|
205
|
+
string = ::String.new(input.to_s)
|
206
|
+
head, *tail = underscore(string).split(CLASSIFY_SEPARATOR)
|
207
|
+
|
208
|
+
tail.unshift(head.capitalize).join(CAPITALIZE_SEPARATOR)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Return a CamelCase version of the string
|
212
|
+
#
|
213
|
+
# @param input [::String] the input
|
214
|
+
#
|
215
|
+
# @return [String] the transformed string
|
216
|
+
#
|
217
|
+
# @since 1.1.0
|
218
|
+
#
|
219
|
+
# @example
|
220
|
+
# require 'hanami/utils/string'
|
221
|
+
#
|
222
|
+
# Hanami::Utils::String.classify('hanami_utils') # => 'HanamiUtils'
|
223
|
+
def self.classify(input)
|
224
|
+
string = ::String.new(input.to_s)
|
225
|
+
words = underscore(string).split(CLASSIFY_WORD_SEPARATOR).map!(&:capitalize)
|
226
|
+
delimiters = underscore(string).scan(CLASSIFY_WORD_SEPARATOR)
|
227
|
+
|
228
|
+
delimiters.map! do |delimiter|
|
229
|
+
delimiter == CLASSIFY_SEPARATOR ? EMPTY_STRING : NAMESPACE_SEPARATOR
|
230
|
+
end
|
231
|
+
|
232
|
+
words.zip(delimiters).join
|
233
|
+
end
|
234
|
+
|
235
|
+
# Return a downcased and underscore separated version of the string
|
236
|
+
#
|
237
|
+
# Revised version of `ActiveSupport::Inflector.underscore` implementation
|
238
|
+
# @see https://github.com/rails/rails/blob/feaa6e2048fe86bcf07e967d6e47b865e42e055b/activesupport/lib/active_support/inflector/methods.rb#L90
|
239
|
+
#
|
240
|
+
# @param input [::String] the input
|
241
|
+
#
|
242
|
+
# @return [String] the transformed string
|
243
|
+
#
|
244
|
+
# @since 1.1.0
|
245
|
+
#
|
246
|
+
# @example
|
247
|
+
# require 'hanami/utils/string'
|
248
|
+
#
|
249
|
+
# Hanami::Utils::String.underscore('HanamiUtils') # => 'hanami_utils'
|
250
|
+
def self.underscore(input)
|
251
|
+
string = ::String.new(input.to_s)
|
252
|
+
string.gsub!(NAMESPACE_SEPARATOR, UNDERSCORE_SEPARATOR)
|
253
|
+
string.gsub!(NAMESPACE_SEPARATOR, UNDERSCORE_SEPARATOR)
|
254
|
+
string.gsub!(/([A-Z\d]+)([A-Z][a-z])/, UNDERSCORE_DIVISION_TARGET)
|
255
|
+
string.gsub!(/([a-z\d])([A-Z])/, UNDERSCORE_DIVISION_TARGET)
|
256
|
+
string.gsub!(/[[:space:]]|\-/, UNDERSCORE_DIVISION_TARGET)
|
257
|
+
string.downcase
|
258
|
+
end
|
259
|
+
|
260
|
+
# Return a downcased and dash separated version of the string
|
261
|
+
#
|
262
|
+
# @param input [::String] the input
|
263
|
+
#
|
264
|
+
# @return [::String] the transformed string
|
265
|
+
#
|
266
|
+
# @since 1.1.0
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# require 'hanami/utils/string'
|
270
|
+
#
|
271
|
+
# Hanami::Utils::String.dasherize('Hanami Utils') # => 'hanami-utils'
|
272
|
+
|
273
|
+
# Hanami::Utils::String.dasherize('hanami_utils') # => 'hanami-utils'
|
274
|
+
#
|
275
|
+
# Hanami::Utils::String.dasherize('HanamiUtils') # => "hanami-utils"
|
276
|
+
def self.dasherize(input)
|
277
|
+
string = ::String.new(input.to_s)
|
278
|
+
underscore(string).split(CLASSIFY_SEPARATOR).join(DASHERIZE_SEPARATOR)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Return the string without the Ruby namespace of the class
|
282
|
+
#
|
283
|
+
# @param input [::String] the input
|
284
|
+
#
|
285
|
+
# @return [String] the transformed string
|
286
|
+
#
|
287
|
+
# @since 1.1.0
|
288
|
+
#
|
289
|
+
# @example
|
290
|
+
# require 'hanami/utils/string'
|
291
|
+
#
|
292
|
+
# Hanami::Utils::String.demodulize('Hanami::Utils::String') # => 'String'
|
293
|
+
#
|
294
|
+
# Hanami::Utils::String.demodulize('String') # => 'String'
|
295
|
+
def self.demodulize(input)
|
296
|
+
::String.new(input.to_s).split(NAMESPACE_SEPARATOR).last
|
297
|
+
end
|
298
|
+
|
299
|
+
# Return the top level namespace name
|
300
|
+
#
|
301
|
+
# @param input [::String] the input
|
302
|
+
#
|
303
|
+
# @return [String] the transformed string
|
304
|
+
#
|
305
|
+
# @since 1.1.0
|
306
|
+
#
|
307
|
+
# @example
|
308
|
+
# require 'hanami/utils/string'
|
309
|
+
#
|
310
|
+
# Hanami::Utils::String.namespace('Hanami::Utils::String') # => 'Hanami'
|
311
|
+
#
|
312
|
+
# Hanami::Utils::String.namespace('String') # => 'String'
|
313
|
+
def self.namespace(input)
|
314
|
+
::String.new(input.to_s).split(NAMESPACE_SEPARATOR).first
|
315
|
+
end
|
316
|
+
|
317
|
+
# Return a pluralized version of self.
|
318
|
+
#
|
319
|
+
# @param input [::String] the input
|
320
|
+
#
|
321
|
+
# @return [::String] the pluralized string.
|
322
|
+
#
|
323
|
+
# @since 1.1.0
|
324
|
+
#
|
325
|
+
# @see Hanami::Utils::Inflector
|
326
|
+
#
|
327
|
+
# @example
|
328
|
+
# require 'hanami/utils/string'
|
329
|
+
#
|
330
|
+
# Hanami::Utils::String.pluralize('book') # => 'books'
|
331
|
+
def self.pluralize(input)
|
332
|
+
string = ::String.new(input.to_s)
|
333
|
+
Inflector.pluralize(string)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Return a singularized version of self.
|
337
|
+
#
|
338
|
+
# @param input [::String] the input
|
339
|
+
#
|
340
|
+
# @return [::String] the singularized string.
|
341
|
+
#
|
342
|
+
# @since 1.1.0
|
343
|
+
#
|
344
|
+
# @see Hanami::Utils::Inflector
|
345
|
+
#
|
346
|
+
# @example
|
347
|
+
# require 'hanami/utils/string'
|
348
|
+
#
|
349
|
+
# Hanami::Utils::String.singularize('books') # => 'book'
|
350
|
+
def self.singularize(input)
|
351
|
+
string = ::String.new(input.to_s)
|
352
|
+
Inflector.singularize(string)
|
353
|
+
end
|
354
|
+
|
355
|
+
# Replace the rightmost match of `pattern` with `replacement`
|
356
|
+
#
|
357
|
+
# If the pattern cannot be matched, it returns the original string.
|
358
|
+
#
|
359
|
+
# This method does NOT mutate the original string.
|
360
|
+
#
|
361
|
+
# @param input [::String] the input
|
362
|
+
# @param pattern [Regexp, ::String] the pattern to find
|
363
|
+
# @param replacement [String] the string to replace
|
364
|
+
#
|
365
|
+
# @return [::String] the replaced string
|
366
|
+
#
|
367
|
+
# @since 1.1.0
|
368
|
+
#
|
369
|
+
# @example
|
370
|
+
# require 'hanami/utils/string'
|
371
|
+
#
|
372
|
+
# Hanami::Utils::String.rsub('authors/books/index', %r{/}, '#')
|
373
|
+
# # => 'authors/books#index'
|
374
|
+
def self.rsub(input, pattern, replacement)
|
375
|
+
string = ::String.new(input.to_s)
|
376
|
+
if i = string.rindex(pattern) # rubocop:disable Lint/AssignmentInCondition
|
377
|
+
s = string.dup
|
378
|
+
s[i] = replacement
|
379
|
+
s
|
380
|
+
else
|
381
|
+
string
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
75
385
|
# Initialize the string
|
76
386
|
#
|
77
387
|
# @param string [::String, Symbol] the value we want to initialize
|
@@ -358,7 +668,7 @@ module Hanami
|
|
358
668
|
@string.scan(pattern, &blk)
|
359
669
|
end
|
360
670
|
|
361
|
-
# Replace the rightmost match of
|
671
|
+
# Replace the rightmost match of `pattern` with `replacement`
|
362
672
|
#
|
363
673
|
# If the pattern cannot be matched, it returns the original string.
|
364
674
|
#
|
data/lib/hanami/utils/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hanami-utils
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.1.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luca Guidi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: transproc
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: concurrent-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,6 +104,7 @@ files:
|
|
90
104
|
- lib/hanami/utils/duplicable.rb
|
91
105
|
- lib/hanami/utils/escape.rb
|
92
106
|
- lib/hanami/utils/file_list.rb
|
107
|
+
- lib/hanami/utils/files.rb
|
93
108
|
- lib/hanami/utils/hash.rb
|
94
109
|
- lib/hanami/utils/inflector.rb
|
95
110
|
- lib/hanami/utils/io.rb
|
@@ -114,12 +129,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
129
|
version: 2.3.0
|
115
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
131
|
requirements:
|
117
|
-
- - "
|
132
|
+
- - ">"
|
118
133
|
- !ruby/object:Gem::Version
|
119
|
-
version:
|
134
|
+
version: 1.3.1
|
120
135
|
requirements: []
|
121
136
|
rubyforge_project:
|
122
|
-
rubygems_version: 2.6.
|
137
|
+
rubygems_version: 2.6.11
|
123
138
|
signing_key:
|
124
139
|
specification_version: 4
|
125
140
|
summary: Ruby core extentions and Hanami utilities
|