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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c65504458f0d4f9fe10074d2375b48bef9beab06
4
- data.tar.gz: d33a0515a65119a9da3f1db4becfdaae5a20602a
3
+ metadata.gz: fe0165282131db30a45dc60b913b4a36f9eb2095
4
+ data.tar.gz: c5ec0be6be92bd58c1e0b6043634b0d0af68e7fd
5
5
  SHA512:
6
- metadata.gz: 0bf23ef0c947d2d7fc88fa750c3077d474beaae7e8812175ccbaf049d731da81044fafc1984f9b4fffd429e3003c6c56bd1d10af9a055cc30b3c2a77e5bf528e
7
- data.tar.gz: 9a24391a94ee3ae43f741e813ff7670d5c9b5978debf30acb1a1cef13c80ec1da421800e5c64c19703f9d4fa4287d37a97b5bc18f5a7e114bb0a8c6f097f6d39
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.4 - 2017-10-02
5
- ### Fixed
6
- - [Luca Guidi] Make `Hanami::Utils::BasicObject` to be fully compatible with Ruby's `pp` and to be inspected by Pry.
7
- - [Thiago Kenji Okada] Fix pluralization/singularization for `"release" => "releases"`
8
-
9
- ## v1.0.3 - 2017-09-06
10
- ### Fixed
11
- - [Malina Sulca] Fix pluralization/singularization for `"exercise" => "exercises"`
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', '~> 1.0'
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'
@@ -145,15 +145,14 @@ module Hanami
145
145
  super
146
146
 
147
147
  base.class_eval do
148
- prepend Interface
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 Interface
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
- case formatter
122
- when Symbol
123
- (subclasses.find { |s| s.eligible?(formatter) } || self).new
124
- when nil
125
- new
126
- else
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}:#{'0x0000%x' % (__id__ << 1)}#{__inspect}>" # rubocop:disable Style/FormatString
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(printer)
47
- printer.text(inspect)
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
@@ -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.
@@ -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 <tt>pattern</tt> with <tt>replacement</tt>
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
  #
@@ -3,6 +3,6 @@ module Hanami
3
3
  # Defines the version
4
4
  #
5
5
  # @since 0.1.0
6
- VERSION = '1.0.4'.freeze
6
+ VERSION = '1.1.0.beta1'.freeze
7
7
  end
8
8
  end
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
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-10-02 00:00:00.000000000 Z
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: '0'
134
+ version: 1.3.1
120
135
  requirements: []
121
136
  rubyforge_project:
122
- rubygems_version: 2.6.13
137
+ rubygems_version: 2.6.11
123
138
  signing_key:
124
139
  specification_version: 4
125
140
  summary: Ruby core extentions and Hanami utilities