yiffspace 0.0.1 → 0.0.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/yiffspace/concerns/active_record_extensions.rb +45 -0
  3. data/lib/yiffspace/concerns/api_methods.rb +101 -0
  4. data/lib/yiffspace/concerns/attribute_matchers.rb +100 -0
  5. data/lib/yiffspace/concerns/attribute_methods.rb +39 -0
  6. data/lib/yiffspace/concerns/concurrency_methods.rb +20 -0
  7. data/lib/yiffspace/concerns/conditional_includes.rb +43 -0
  8. data/lib/yiffspace/concerns/current_methods.rb +60 -0
  9. data/lib/yiffspace/concerns/has_bit_flags.rb +59 -0
  10. data/lib/yiffspace/concerns/user_methods.rb +77 -0
  11. data/lib/yiffspace/concerns/user_name_methods.rb +53 -0
  12. data/lib/yiffspace/configuration.rb +70 -10
  13. data/lib/yiffspace/core_ext/all.rb +0 -1
  14. data/lib/yiffspace/include/all.rb +16 -0
  15. data/lib/yiffspace/include/cache.rb +5 -0
  16. data/lib/yiffspace/include/current.rb +5 -0
  17. data/lib/yiffspace/include/duration_parser.rb +5 -0
  18. data/lib/yiffspace/include/helpers.rb +5 -0
  19. data/lib/yiffspace/{core_ext → include}/open_hash.rb +2 -0
  20. data/lib/yiffspace/include/parameter_builder.rb +5 -0
  21. data/lib/yiffspace/include/parse_value.rb +5 -0
  22. data/lib/yiffspace/include/query_builder.rb +5 -0
  23. data/lib/yiffspace/include/query_dsl.rb +5 -0
  24. data/lib/yiffspace/include/query_helper.rb +5 -0
  25. data/lib/yiffspace/include/routes.rb +5 -0
  26. data/lib/yiffspace/include/table_builder.rb +5 -0
  27. data/lib/yiffspace/include/trace_logger.rb +5 -0
  28. data/lib/yiffspace/include/user_attribute.rb +5 -0
  29. data/lib/yiffspace/search/query_builder.rb +83 -0
  30. data/lib/yiffspace/search/query_dsl.rb +119 -0
  31. data/lib/yiffspace/search/query_helper.rb +49 -0
  32. data/lib/yiffspace/utils/cache.rb +40 -0
  33. data/lib/yiffspace/utils/current.rb +43 -0
  34. data/lib/yiffspace/utils/duration_parser.rb +24 -0
  35. data/lib/yiffspace/utils/helpers.rb +20 -0
  36. data/lib/yiffspace/utils/parameter_builder.rb +121 -0
  37. data/lib/yiffspace/utils/parse_value.rb +174 -0
  38. data/lib/yiffspace/utils/routes.rb +28 -0
  39. data/lib/yiffspace/utils/table_builder.rb +136 -0
  40. data/lib/yiffspace/utils/trace_logger.rb +91 -0
  41. data/lib/yiffspace/utils/user_attribute.rb +271 -0
  42. data/lib/yiffspace/version.rb +1 -1
  43. data/lib/yiffspace.rb +11 -1
  44. metadata +68 -3
@@ -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,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ # A helper class for building HTML tables. Used in views.
6
+ #
7
+ # @example
8
+ # <%= table_for @tags do |table| %>
9
+ # <% table.column :name do |tag| %>
10
+ # <%= link_to_wiki "?", tag.name %>
11
+ # <%= link_to tag.name, posts_path(tags: tag.name) %>
12
+ # <% end %>
13
+ # <% table.column :post_count %>
14
+ # <% end %>
15
+ #
16
+ # @see app/views/table_builder/_table.html.erb
17
+ class TableBuilder
18
+ # Represents a single column in the table.
19
+ class Column
20
+ attr_reader(:attribute, :name, :block, :header_attributes, :body_attributes, :caption)
21
+
22
+ # Define a table column.
23
+ #
24
+ # @example
25
+ # <% table.column :post_count %>
26
+ #
27
+ # @example
28
+ # <% table.column :name do |tag| %>
29
+ # <%= tag.pretty_name %>
30
+ # <% end %>
31
+ #
32
+ # @param attribute [Symbol] The attribute in the model the column is for.
33
+ # The column's name and value will come from this attribute by default.
34
+ # @param name [String] the column's name, if different from the attribute name.
35
+ # @param column [String] the column name
36
+ # @param th [Hash] the HTML attributes for the column's <th> tag.
37
+ # @param td [Hash] the HTML attributes for the column's <td> tag.
38
+ # @param width [String] the HTML width value for the <th> tag.
39
+ # @yieldparam item a block that returns the column value based on the item.
40
+ def initialize(attribute = nil, column: nil, th: {}, td: {}, width: nil, name: nil, &block)
41
+ @attribute = attribute
42
+ @column = column
43
+ @header_attributes = { width: width, **th }
44
+ @body_attributes = td
45
+ @block = block
46
+
47
+ @name = name || attribute
48
+ @name = @name.to_s.titleize unless @name.is_a?(String)
49
+
50
+ return unless @name.present? || @column.present?
51
+
52
+ if @column.present?
53
+ column_class = "#{@column}-column"
54
+ else
55
+ column_class = "#{@name.parameterize.dasherize}-column"
56
+ end
57
+ @header_attributes[:class] = "#{column_class} #{@header_attributes[:class]}".strip
58
+ @body_attributes[:class] = "#{column_class} #{@body_attributes[:class]}".strip
59
+ end
60
+
61
+ # Returns the value of the table cell.
62
+ # @param item [ApplicationRecord] the table cell item
63
+ # @param row [Integer] the table row number
64
+ # @param column [Integer] the table column number
65
+ # @return [#to_s] the value of the table cell
66
+ def value(item, row, column)
67
+ if block.present?
68
+ block.call(item, row, column, self)
69
+ nil
70
+ elsif attribute.is_a?(Symbol)
71
+ item.send(attribute)
72
+ else
73
+ ""
74
+ end
75
+ end
76
+ end
77
+
78
+ attr_reader(:columns, :table_attributes, :row_attributes, :items)
79
+
80
+ # Build a table for an array of objects, one object per row.
81
+ #
82
+ # The <table> tag is automatically given an HTML id of the form `{name}-table`.
83
+ # For example, `posts-table`, `tags-table`.
84
+ #
85
+ # The <tr> tag is automatically given an HTML id of the form `{name}-{id}`.
86
+ # For example, `post-1234`, `tag-4567`, etc. Each <tr> tag also gets a set of
87
+ # data attributes for each model; see #html_data_attributes in app/policies.
88
+ #
89
+ # @param items [Array<ApplicationRecord>] The list of ActiveRecord objects to
90
+ # build the table for. One item per table row.
91
+ # @param tr [Hash] optional HTML attributes for the <tr> tag for each row
92
+ # @param table_attributes [Hash] optional HTML attributes for the <table> tag
93
+ # @yieldparam table [self] the table being built
94
+ def initialize(items, tr: {}, **table_attributes)
95
+ @items = items
96
+ @columns = []
97
+ @table_attributes = { class: "striped", **table_attributes }
98
+ @row_attributes = tr
99
+
100
+ @table_attributes[:id] ||= "#{items.model_name.plural.dasherize}-table" if items.respond_to?(:model_name)
101
+
102
+ yield(self) if block_given?
103
+ end
104
+
105
+ # Set the table caption
106
+ def caption
107
+ @caption = yield if block_given?
108
+ @caption
109
+ end
110
+
111
+ # Add a column to the table.
112
+ # @example
113
+ # table.column(:name)
114
+ def column(*, **options, &)
115
+ opt = options.extract!(:if, :unless)
116
+ return if (opt.key?(:if) && !opt[:if]) || (opt.key?(:unless) && opt[:unless])
117
+
118
+ @columns << Column.new(*, **options, &)
119
+ end
120
+
121
+ # Return the HTML attributes for each <tr> tag.
122
+ # @param item [ApplicationRecord] the item for this row
123
+ # @param _row [Integer] the row number (unused)
124
+ # @return [Hash] the <tr> attributes
125
+ def all_row_attributes(item, _row)
126
+ return {} unless item.is_a?(ApplicationRecord)
127
+
128
+ {
129
+ id: "#{item.model_name.singular.dasherize}-#{item.id}",
130
+ **row_attributes,
131
+ **ApplicationController.helpers.data_attributes_for(item),
132
+ }
133
+ end
134
+ end
135
+ end
136
+ 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