hanami-utils 1.0.4 → 1.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|