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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +20 -0
- data/README.md +27 -0
- data/Rakefile +16 -0
- data/app/assets/config/yiffspace_manifest.js +1 -0
- data/app/assets/stylesheets/yiffspace/application.css +30 -0
- data/app/controllers/yiff_space/application_controller.rb +6 -0
- data/app/helpers/yiff_space/application_helper.rb +6 -0
- data/app/models/yiff_space/application_record.rb +7 -0
- data/app/views/layouts/yiff_space/application.html.erb +17 -0
- data/app/views/yiff_space/error.html.erb +3 -0
- data/lib/yiffspace/concerns/active_record_extensions.rb +45 -0
- data/lib/yiffspace/concerns/api_methods.rb +101 -0
- data/lib/yiffspace/concerns/attribute_matchers.rb +100 -0
- data/lib/yiffspace/concerns/attribute_methods.rb +39 -0
- data/lib/yiffspace/concerns/concurrency_methods.rb +20 -0
- data/lib/yiffspace/concerns/conditional_includes.rb +43 -0
- data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
- data/lib/yiffspace/configuration.rb +43 -0
- data/lib/yiffspace/core/version.rb +7 -0
- data/lib/yiffspace/core.rb +30 -0
- data/lib/yiffspace/engine.rb +13 -0
- data/lib/yiffspace/utils/cache.rb +40 -0
- data/lib/yiffspace/utils/duration_parser.rb +24 -0
- data/lib/yiffspace/utils/helpers.rb +25 -0
- data/lib/yiffspace/utils/open_hash.rb +63 -0
- data/lib/yiffspace/utils/parameter_builder.rb +121 -0
- data/lib/yiffspace/utils/parse_value.rb +174 -0
- data/lib/yiffspace/utils/routes.rb +28 -0
- data/lib/yiffspace/utils/set_env_constraint.rb +18 -0
- data/lib/yiffspace/utils/trace_logger.rb +91 -0
- data/lib/yiffspace/utils.rb +6 -0
- 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
|