yiffspace-core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE +20 -0
  4. data/README.md +27 -0
  5. data/Rakefile +16 -0
  6. data/app/assets/config/yiffspace_manifest.js +1 -0
  7. data/app/assets/stylesheets/yiffspace/application.css +30 -0
  8. data/app/controllers/yiff_space/application_controller.rb +6 -0
  9. data/app/helpers/yiff_space/application_helper.rb +6 -0
  10. data/app/models/yiff_space/application_record.rb +7 -0
  11. data/app/views/layouts/yiff_space/application.html.erb +17 -0
  12. data/app/views/yiff_space/error.html.erb +3 -0
  13. data/lib/yiffspace/concerns/active_record_extensions.rb +45 -0
  14. data/lib/yiffspace/concerns/api_methods.rb +101 -0
  15. data/lib/yiffspace/concerns/attribute_matchers.rb +100 -0
  16. data/lib/yiffspace/concerns/attribute_methods.rb +39 -0
  17. data/lib/yiffspace/concerns/concurrency_methods.rb +20 -0
  18. data/lib/yiffspace/concerns/conditional_includes.rb +43 -0
  19. data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
  20. data/lib/yiffspace/configuration.rb +43 -0
  21. data/lib/yiffspace/core/version.rb +7 -0
  22. data/lib/yiffspace/core.rb +30 -0
  23. data/lib/yiffspace/engine.rb +13 -0
  24. data/lib/yiffspace/utils/cache.rb +40 -0
  25. data/lib/yiffspace/utils/duration_parser.rb +24 -0
  26. data/lib/yiffspace/utils/helpers.rb +25 -0
  27. data/lib/yiffspace/utils/open_hash.rb +63 -0
  28. data/lib/yiffspace/utils/parameter_builder.rb +121 -0
  29. data/lib/yiffspace/utils/parse_value.rb +174 -0
  30. data/lib/yiffspace/utils/routes.rb +28 -0
  31. data/lib/yiffspace/utils/set_env_constraint.rb +18 -0
  32. data/lib/yiffspace/utils/trace_logger.rb +91 -0
  33. data/lib/yiffspace/utils.rb +6 -0
  34. metadata +135 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("zeitwerk")
4
+ require("active_support/all")
5
+ require("active_support/core_ext/object/blank")
6
+
7
+ module YiffSpace
8
+ module Core
9
+ end
10
+
11
+ class << self
12
+ def config
13
+ @config ||= Configuration.new
14
+ end
15
+
16
+ def configure
17
+ yield(config)
18
+ end
19
+ end
20
+ end
21
+
22
+ loader = Zeitwerk::Loader.for_gem_extension(YiffSpace)
23
+ loader.ignore("#{__dir__}/engine.rb")
24
+ loader.setup
25
+
26
+ # Require the engine eagerly so it registers with Rails before the host app's
27
+ # active_support.initialize_per_engine_zeitwerk_loaders initializer runs. Without this,
28
+ # the engine is only loaded lazily (after Zeitwerk setup) and its app/controllers
29
+ # path is never added to the app's autoload roots.
30
+ require_relative("engine") if defined?(Rails)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("rails")
4
+
5
+ module YiffSpace
6
+ class Engine < ::Rails::Engine
7
+ config.root = File.expand_path("../..", __dir__)
8
+
9
+ initializer("yiffspace.assets") do |app|
10
+ app.config.assets.precompile += %w[yiffspace/application.css] if app.config.respond_to?(:assets)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ module Cache
6
+ module_function
7
+
8
+ def read_multi(keys, prefix)
9
+ sanitized_key_to_key_hash = keys.index_by { |key| "#{prefix}:#{key}" }
10
+
11
+ sanitized_keys = sanitized_key_to_key_hash.keys
12
+ sanitized_key_to_value_hash = Rails.cache.read_multi(*sanitized_keys)
13
+
14
+ sanitized_key_to_value_hash.transform_keys(&sanitized_key_to_key_hash)
15
+ end
16
+
17
+ def fetch(key, expires_in: nil, &)
18
+ Rails.cache.fetch(key, expires_in: expires_in, &)
19
+ end
20
+
21
+ def write(key, value, expires_in: nil)
22
+ Rails.cache.write(key, value, expires_in: expires_in)
23
+ end
24
+
25
+ def delete(key)
26
+ Rails.cache.delete(key)
27
+ end
28
+
29
+ def clear
30
+ Rails.cache.clear
31
+ end
32
+
33
+ def redis
34
+ # Using a shared variable like this here is OK
35
+ # since pitchfork spawns a new process for each worker
36
+ @redis ||= Redis.new(url: YiffSpace.config.redis_url.call)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("abbrev")
4
+
5
+ module YiffSpace
6
+ module Utils
7
+ module DurationParser
8
+ def self.parse(string)
9
+ abbrevs = Abbrev.abbrev(%w[seconds minutes hours days weeks months years])
10
+
11
+ raise unless string =~ /(.*?)([a-z]+)\z/i
12
+
13
+ size = Float($1)
14
+ unit = abbrevs.fetch($2.downcase)
15
+
16
+ raise(NotImplementedError) unless %w[seconds minutes hours days weeks months years].include?(unit)
17
+
18
+ size.public_send(unit)
19
+ rescue # rubocop:disable Style/RescueStandardError
20
+ raise(ArgumentError, "'#{string}' is not a valid duration")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ module Helpers
6
+ module_function
7
+
8
+ # Uses send/respond_to?(name, true) (not public_send/respond_to?(name)) because host apps
9
+ # commonly call helpers through this proxy with an explicit receiver (e.g. a decorator's
10
+ # `h.some_helper`), including ones marked private in their helper module - private only
11
+ # restricts calling with an explicit receiver, and normal view rendering (which invokes
12
+ # helpers without one) isn't affected either way.
13
+ def method_missing(name, *, &)
14
+ helpers = YiffSpace.config.application_controller_class.helpers
15
+ return helpers.send(name, *, &) if helpers.respond_to?(name, true)
16
+
17
+ super
18
+ end
19
+
20
+ def respond_to_missing?(name, include_private = false)
21
+ YiffSpace.config.application_controller_class.helpers.respond_to?(name, true) || super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("active_support/hash_with_indifferent_access")
4
+
5
+ module YiffSpace
6
+ module Utils
7
+ class OpenHash < ActiveSupport::HashWithIndifferentAccess
8
+ def initialize(constructor = nil, respond_to_missing: true)
9
+ super(constructor)
10
+ @respond_to_missing = respond_to_missing
11
+ end
12
+
13
+ def respond_to_missing?(name, include_private = false)
14
+ return true if @respond_to_missing
15
+
16
+ name = name.to_s
17
+ key?(name) || name.end_with?("=") || super
18
+ end
19
+
20
+ def method_missing(name, *args)
21
+ if name.end_with?("=")
22
+ public_send(:[]=, name.to_s.chomp("=").to_sym, args.first)
23
+ elsif key?(name)
24
+ public_send(:[], name)
25
+ elsif @respond_to_missing
26
+ nil
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def self.from(hash = nil, recursive: true, respond_to_missing: true, **kwargs)
33
+ hash = kwargs if hash.nil? && kwargs.any?
34
+ raise(ArgumentError, "no hash provided") if hash.nil?
35
+
36
+ oh = OpenHash.new(respond_to_missing: respond_to_missing)
37
+ hash.each do |key, value|
38
+ if recursive && value.is_a?(Hash)
39
+ oh[key] = OpenHash.from(value, recursive: recursive)
40
+ elsif recursive && value.is_a?(Array)
41
+ oh[key] = value.map { |v| v.is_a?(Hash) ? OpenHash.from(v, recursive: recursive, respond_to_missing: respond_to_missing) : v }
42
+ else
43
+ oh[key] = value
44
+ end
45
+ end
46
+ oh
47
+ end
48
+
49
+ def marshal_dump
50
+ [to_h, @respond_to_missing]
51
+ end
52
+
53
+ def marshal_load(data)
54
+ hash, @respond_to_missing = data
55
+ replace(hash)
56
+ end
57
+
58
+ def self.from_array(items, **)
59
+ items.map { |item| from(item, **) }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ class ParameterBuilder
6
+ def self.serial_parameters(only_string, object, options = {})
7
+ only_array = split_only_string(only_string)
8
+ get_only_hash(only_array, object, options)
9
+ end
10
+
11
+ def self.get_only_hash(only_array, object, options = {}, seen_objects = [])
12
+ return {} if object.nil?
13
+
14
+ is_root = seen_objects.empty?
15
+ only_hash = { only: [], include: [], methods: [] }
16
+ available_includes = object.available_includes
17
+ attributes, methods = object.api_attributes(options[:user]).partition { |attr| object.has_attribute?(attr) }
18
+ methods -= available_includes
19
+ # Attributes and/or methods may be included in the final pass, but not includes
20
+ seen_objects << object.class.name
21
+ underscore = false
22
+ only_array.each do |item|
23
+ if item == "_"
24
+ underscore = true
25
+ next
26
+ end
27
+ match = item.match(/(\w+)\[(.+?)\]$/)
28
+ item = (match || [])[1] || item
29
+ item_sym = item.to_sym
30
+ was_seen = inclusion_seen?(item, object.class, seen_objects)
31
+ if match && available_includes.include?(item_sym) && (!was_seen || is_root)
32
+ item_object = object.send(item_sym)
33
+ next if item_object.nil?
34
+
35
+ item_object = item_object[0] if item_object.is_a?(ActiveRecord::Relation)
36
+ item_array = split_only_string(match[2])
37
+ item_hash = get_only_hash(item_array, item_object, options, seen_objects.clone)
38
+ only_hash[:include] << { item_sym => item_hash }
39
+ elsif available_includes.include?(item_sym) && (!was_seen || is_root)
40
+ only_hash[:include] << item_sym
41
+ elsif attributes.include?(item_sym)
42
+ only_hash[:only] << item_sym
43
+ elsif methods.include?(item_sym)
44
+ only_hash[:methods] << item_sym
45
+ only_hash[:only] << item_sym
46
+ end
47
+ end
48
+ only_hash.delete(:include) if only_hash[:include].empty?
49
+ only_hash.delete(:methods) if only_hash[:methods].empty?
50
+ only_hash[:only].unshift("_") if underscore
51
+ only_hash
52
+ end
53
+
54
+ def self.includes_parameters(only_string, model_name)
55
+ return [] if only_string.blank?
56
+
57
+ only_array = split_only_string(only_string)
58
+ get_includes_array(only_array, model_name)
59
+ end
60
+
61
+ def self.get_includes_array(only_array, model_name, seen_objects = [])
62
+ is_root = seen_objects.empty?
63
+ include_array = []
64
+ model = Kernel.const_get(model_name)
65
+ available_includes = model.available_includes
66
+ # Attributes and/or methods may be included in the final pass, but not includes
67
+ seen_objects << model_name
68
+ only_array.each do |item|
69
+ match = item.match(/(\w+)\[(.+?)\]$/)
70
+ item = (match || [])[1] || item
71
+ item_sym = item.to_sym
72
+ was_seen = inclusion_seen?(item, model, seen_objects)
73
+ if match && available_includes.include?(item_sym) && (!was_seen || is_root)
74
+ item_array = split_only_string(match[2])
75
+ model.associated_models(item).each do |m|
76
+ item_array = get_includes_array(item_array, m, seen_objects.clone)
77
+ include_array << (item_array.empty? ? item_sym : { item_sym => item_array })
78
+ end
79
+ elsif available_includes.include?(item_sym) && (!was_seen || is_root)
80
+ include_array << item_sym
81
+ end
82
+ end
83
+ include_array
84
+ end
85
+
86
+ def self.inclusion_seen?(inclusion, class_object, seen_objects)
87
+ if class_object.reflections[inclusion]
88
+ inclusion_class = class_object.reflections[inclusion].class_name
89
+ max_seen = (class_object.multiple_includes.include?(inclusion.to_sym) ? 1 : 0)
90
+ seen_objects.count(inclusion_class) > max_seen
91
+ else
92
+ false
93
+ end
94
+ end
95
+
96
+ def self.split_only_string(only_string)
97
+ only_array = []
98
+ offset = 0
99
+ position = 0
100
+ level = 0
101
+ loop do
102
+ str = only_string[Range.new(position, -1)]
103
+ match = str.match(/[,\[\]]/)
104
+ break unless match
105
+
106
+ start_pos, end_pos = match.offset(0)
107
+ if match[0] == "," && level.zero?
108
+ only_array << only_string[Range.new(offset, position + start_pos - 1)]
109
+ offset = position + end_pos
110
+ elsif match[0] == "["
111
+ level += 1
112
+ elsif match[0] == "]"
113
+ level -= 1
114
+ end
115
+ position += end_pos
116
+ end
117
+ only_array << only_string[Range.new(offset, -1)]
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ module ParseValue
6
+ MAX_INT = 2_147_483_647
7
+ MIN_INT = -2_147_483_648
8
+ extend(self)
9
+
10
+ def date_range(target)
11
+ case target
12
+ # 10_yesterweeks_ago, 10yesterweekago
13
+ when /\A(\d{1,2})_?yester(week|month|year)s?_?ago\z/
14
+ yester_range($1.to_i, $2)
15
+ when /\Ayester(week|month|year)\z/
16
+ yester_range(1, $1)
17
+ when /\A(day|week|month|year)\z/
18
+ [:gte, Time.zone.now - 1.send($1)]
19
+ # 10_weeks_ago, 10w
20
+ when /\A(\d+)_?(s(econds?)?|mi(nutes?)?|h(ours?)?|d(ays?)?|w(eeks?)?|mo(nths?)?|y(ears?)?)_?(ago)?\z/i
21
+ [:gte, time_string(target)]
22
+ else
23
+ range(target, :date)
24
+ end
25
+ end
26
+
27
+ def range_fudged(range, type)
28
+ result = range(range, type)
29
+ if result[0] == :eq
30
+ new_min = [(result[1] * 0.95).to_i, MIN_INT].max
31
+ new_max = [(result[1] * 1.05).to_i, MAX_INT].min
32
+ [:between, new_min, new_max]
33
+ else
34
+ result
35
+ end
36
+ end
37
+
38
+ def range(range, type = :integer)
39
+ if range.start_with?("<=")
40
+ [:lte, cast(range.delete_prefix("<="), type)]
41
+
42
+ elsif range.start_with?("..")
43
+ [:lte, cast(range.delete_prefix(".."), type)]
44
+
45
+ elsif range.start_with?("<")
46
+ [:lt, cast(range.delete_prefix("<"), type)]
47
+
48
+ elsif range.start_with?(">=")
49
+ [:gte, cast(range.delete_prefix(">="), type)]
50
+
51
+ elsif range.end_with?("..")
52
+ [:gte, cast(range.delete_suffix(".."), type)]
53
+
54
+ elsif range.start_with?(">")
55
+ [:gt, cast(range.delete_prefix(">"), type)]
56
+
57
+ elsif range.include?("..")
58
+ left, right = range.split("..", 2)
59
+ [:between, cast(left, type), cast(right, type)]
60
+
61
+ elsif range.include?(",")
62
+ [:in, range.split(",").first(YiffSpace.config.max_multi_count.call).map { |x| cast(x, type) }]
63
+
64
+ else
65
+ [:eq, cast(range, type)]
66
+
67
+ end
68
+ end
69
+
70
+ RANGE_INVERSIONS = {
71
+ lte: :gte,
72
+ lt: :gt,
73
+ gte: :lte,
74
+ gt: :lt,
75
+ }.freeze
76
+
77
+ def invert_range(range)
78
+ # >10 <=> <10
79
+ range[0] = RANGE_INVERSIONS[range[0]] || range[0]
80
+ # 10..20 <=> 20..10
81
+ range[1], range[2] = range[2], range[1] if range[0] == :between
82
+ range
83
+ end
84
+
85
+ private
86
+
87
+ def cast(object, type)
88
+ case type
89
+ when :integer
90
+ object.to_i.clamp(MIN_INT, MAX_INT)
91
+
92
+ when :float
93
+ # Floats obviously have a different range but this is good enough
94
+ object.to_f.clamp(MIN_INT, MAX_INT)
95
+
96
+ when :date, :datetime
97
+ case object
98
+ when "today"
99
+ return Date.current
100
+ when "yesterday"
101
+ return Date.yesterday
102
+ when "decade"
103
+ return Date.current - 10.years
104
+ when /\A(day|week|month|year)\z/
105
+ return Date.current - 1.send($1.to_sym)
106
+ end
107
+
108
+ ago = time_string(object)
109
+ return ago if ago.present?
110
+
111
+ begin
112
+ Time.zone.parse(object)
113
+ rescue ArgumentError
114
+ nil
115
+ end
116
+
117
+ when :age
118
+ time_string(object)
119
+
120
+ when :ratio
121
+ left, right = object.split(":", 10)
122
+
123
+ if right && right.to_f != 0.0
124
+ (left.to_f / right.to_f).round(10)
125
+ elsif right
126
+ 0.0
127
+ else
128
+ object.to_f.round(2)
129
+ end
130
+
131
+ when :filesize
132
+ size = object.downcase
133
+ if size.end_with?("kb")
134
+ size.to_f.kilobytes
135
+ elsif size.end_with?("mb")
136
+ size.to_f.megabytes
137
+ else
138
+ size.to_f
139
+ end.to_i
140
+ end
141
+ end
142
+
143
+ def yester_range(count, unit)
144
+ origin = Date.current - count.send(unit)
145
+ start = origin.send("beginning_of_#{unit}")
146
+ stop = origin.send("end_of_#{unit}")
147
+ [:between, start, stop]
148
+ end
149
+
150
+ def time_string(target)
151
+ target =~ /\A(\d+)_?(s(econds?)?|mi(nutes?)?|h(ours?)?|d(ays?)?|w(eeks?)?|mo(nths?)?|y(ears?)?)_?(ago)?\z/i
152
+
153
+ size = $1.to_i
154
+ unit = $2&.downcase || ""
155
+
156
+ if unit.start_with?("s")
157
+ size.seconds.ago
158
+ elsif unit.start_with?("mi")
159
+ size.minutes.ago
160
+ elsif unit.start_with?("h")
161
+ size.hours.ago
162
+ elsif unit.start_with?("d")
163
+ size.days.ago
164
+ elsif unit.start_with?("w")
165
+ size.weeks.ago
166
+ elsif unit.start_with?("mo")
167
+ size.months.ago
168
+ elsif unit.start_with?("y")
169
+ size.years.ago
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ # Allow Rails URL helpers to be used outside of views.
6
+ #
7
+ # @example
8
+ # Routes.posts_path(tags: "male")
9
+ # => "/posts?tags=male"
10
+ #
11
+ # @see config/routes.rb
12
+ # @see https://guides.rubyonrails.org/routing.html
13
+ module Routes
14
+ module_function
15
+
16
+ def method_missing(name, *, &)
17
+ url_helpers = Rails.application.routes.url_helpers
18
+ return url_helpers.public_send(name, *, &) if url_helpers.respond_to?(name)
19
+
20
+ super
21
+ end
22
+
23
+ def respond_to_missing?(name, include_private = false)
24
+ Rails.application.routes.url_helpers.respond_to?(name, include_private) || super
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ class SetEnvConstraint
6
+ attr_reader(:key, :value)
7
+
8
+ def initialize(key, value)
9
+ @key = key.to_s
10
+ @value = value
11
+ end
12
+
13
+ def matches?(request)
14
+ request.env[key.to_s] = value
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ module TraceLogger
6
+ module_function
7
+
8
+ COLORS = {
9
+ black: "\e[30m",
10
+ red: "\e[31m",
11
+ green: "\e[32m",
12
+ yellow: "\e[33m",
13
+ blue: "\e[34m",
14
+ magenta: "\e[35m",
15
+ cyan: "\e[36m",
16
+ white: "\e[37m",
17
+ reset: "\e[0m",
18
+ }.freeze
19
+
20
+ # noinspection RubyLiteralArrayInspection
21
+ LEVELS = {
22
+ debug: ["%<cyan>s", "%<blue>s"],
23
+ error: ["%<red>s", "%<red>s"],
24
+ info: ["%<cyan>s", "%<blue>s"],
25
+ warn: ["%<yellow>s", "%<yellow>s"],
26
+ default: ["%<white>s", "%<white>s"],
27
+ }.freeze
28
+
29
+ def format_level(level)
30
+ level = level.to_sym
31
+ primary, alternate = LEVELS.fetch(level, LEVELS[:default])
32
+ colorize("#{alternate}[%<reset>s#{primary}#{level.to_s.upcase}%<reset>s#{alternate}]%<reset>s")
33
+ end
34
+
35
+ def colorize(text, **)
36
+ format(text, **COLORS, **)
37
+ end
38
+
39
+ def debug(*, **)
40
+ _log(*, level: :debug, **)
41
+ end
42
+
43
+ def error(*, **)
44
+ _log(*, level: :error, **)
45
+ end
46
+
47
+ def info(*, **)
48
+ _log(*, level: :info, **)
49
+ end
50
+
51
+ def warn(*, **)
52
+ _log(*, level: :warn, **)
53
+ end
54
+
55
+ def _log(*arg, ignore: nil, level: :log, lines: 3, format: nil)
56
+ return unless Rails.logger.public_send("#{level}?")
57
+
58
+ if arg.one?
59
+ name = nil
60
+ message = arg.first
61
+ else
62
+ name = arg.shift
63
+ message = arg.join
64
+ end
65
+ primary, alternate = LEVELS.fetch(level, LEVELS[:default])
66
+ args = { level: format_level(level), name: name, message: message }
67
+ fmt = "%<level>s"
68
+ if format.nil?
69
+ if name.present?
70
+ fmt += " #{alternate}[%<reset>s%<magenta>s%<name>s%<reset>s#{alternate}]%<reset>s " \
71
+ "#{primary}%<message>s%<reset>s"
72
+ else
73
+ fmt += " #{primary}%<message>s%<reset>s"
74
+ end
75
+ else
76
+ fmt += " #{format}%<reset>s"
77
+ end
78
+ ignore = Array(ignore).unshift(%r{/yiffspace/utils/trace_logger\.rb})
79
+ callers = caller_locations.reject do |loc|
80
+ path = loc.absolute_path || loc.path
81
+ !Rails.backtrace_cleaner.clean_frame("#{path}:#{loc.lineno}") || ignore.any? { |i| path.match?(i) }
82
+ end
83
+ callers = callers.take(lines) if lines.present?
84
+ Rails.logger.public_send(level, colorize(fmt, **args))
85
+ callers.each { |c| Rails.logger.public_send(level, "↳ #{c.path.gsub(%r{^/app/}, '')}:#{c.lineno} in `#{c.label}`") }
86
+ end
87
+
88
+ private_class_method(:_log)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ end
6
+ end