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 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