yiffspace 0.0.1 → 0.0.2

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 +24 -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 +32 -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
@@ -2,6 +2,76 @@
2
2
 
3
3
  module YiffSpace
4
4
  class Configuration
5
+ # Maximum number of comma-separated values allowed in a multi-value query parameter.
6
+ # Used by ParseValue.range and QueryBuilder.
7
+ attr_reader(:max_multi_count)
8
+
9
+ # Redis URL used by Utils::Cache for direct Redis connections.
10
+ attr_reader(:redis_url)
11
+
12
+ # The application's User model class. Used by Utils::Current, Search::QueryDSL, etc.
13
+ # Falls back to ::User at call time when nil.
14
+ attr_accessor(:user_class)
15
+
16
+ # The application's UserResolvable class. Used by Utils::Current.
17
+ # Falls back to ::UserResolvable at call time when nil.
18
+ attr_accessor(:user_resolvable_class)
19
+
20
+ # The CurrentAttributes class used to access the current user in model concerns.
21
+ # Defaults to YiffSpace::Utils::Current.
22
+ attr_writer(:current_class)
23
+
24
+ # The default IP address assigned to Utils::Current when none is present.
25
+ attr_accessor(:default_ip_address)
26
+
27
+ # Override the proc used to fetch the anonymous user. Must respond to #call.
28
+ # Default: -> { (user_class || ::User).anonymous }
29
+ attr_writer(:anonymous_user_getter)
30
+
31
+ # Override the proc used to fetch the system user. Must respond to #call.
32
+ # Default: -> { (user_class || ::User).system }
33
+ attr_writer(:system_user_getter)
34
+
35
+ # The anonymous user name
36
+ attr_reader(:anonymous_user_name)
37
+
38
+ def initialize
39
+ @max_multi_count = -> { 100 }
40
+ @redis_url = -> {}
41
+ @user_class = nil
42
+ @user_resolvable_class = nil
43
+ @current_class = nil
44
+ @default_ip_address = "127.0.0.1"
45
+ @anonymous_user_name = -> { "Anonymous" }
46
+ end
47
+
48
+ # Returns the configured current class, defaulting to YiffSpace::Utils::Current.
49
+ def current_class
50
+ @current_class || Utils::Current
51
+ end
52
+
53
+ # Lazily built: calls user_class (or ::User) at invocation time, not config time.
54
+ def anonymous_user_getter
55
+ @anonymous_user_getter ||= -> { (user_class || ::User).anonymous }
56
+ end
57
+
58
+ # Lazily built: calls user_class (or ::User) at invocation time, not config time.
59
+ def system_user_getter
60
+ @system_user_getter ||= -> { (user_class || ::User).system }
61
+ end
62
+
63
+ def redis_url=(value)
64
+ @redis_url = value.is_a?(Proc) ? value : -> { value }
65
+ end
66
+
67
+ def max_multi_count=(value)
68
+ @max_multi_count = value.is_a?(Proc) ? value : -> { value }
69
+ end
70
+
71
+ def anonymous_user_name=(value)
72
+ @anonymous_user_name = value.is_a?(Proc) ? value : -> { value }
73
+ end
74
+
5
75
  def auth(&block)
6
76
  client = YiffSpace::Auth.register(Auth::DEFAULT_CLIENT_NAME) unless YiffSpace::Auth.instance_variable_get(:@clients).key?(Auth::DEFAULT_CLIENT_NAME)
7
77
  client ||= YiffSpace::Auth[Auth::DEFAULT_CLIENT_NAME]
@@ -17,14 +87,4 @@ module YiffSpace
17
87
  @images ||= Images.new
18
88
  end
19
89
  end
20
-
21
- class << self
22
- def config
23
- @config ||= Configuration.new
24
- end
25
-
26
- def configure
27
- yield(config)
28
- end
29
- end
30
90
  end
@@ -5,4 +5,3 @@ require_relative("arel/all")
5
5
  require_relative("enumerable/all")
6
6
  require_relative("hash/all")
7
7
  require_relative("string/all")
8
- require_relative("open_hash")
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("cache")
4
+ require_relative("current")
5
+ require_relative("duration_parser")
6
+ require_relative("helpers")
7
+ require_relative("open_hash")
8
+ require_relative("parameter_builder")
9
+ require_relative("parse_value")
10
+ require_relative("query_builder")
11
+ require_relative("query_dsl")
12
+ require_relative("query_helper")
13
+ require_relative("routes")
14
+ require_relative("table_builder")
15
+ require_relative("trace_logger")
16
+ require_relative("user_attribute")
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/cache")
4
+
5
+ Cache = YiffSpace::Utils::Cache
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/current")
4
+
5
+ Current = YiffSpace::Utils::Current
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/duration_parser")
4
+
5
+ DurationParser = YiffSpace::Utils::DurationParser
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/helpers")
4
+
5
+ Helpers = YiffSpace::Utils::Helpers
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative("../utils/open_hash")
4
+
3
5
  OpenHash = YiffSpace::Utils::OpenHash
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/parameter_builder")
4
+
5
+ ParameterBuilder = YiffSpace::Utils::ParameterBuilder
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/parse_value")
4
+
5
+ ParseValue = YiffSpace::Utils::ParseValue
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../search/query_builder")
4
+
5
+ QueryBuilder = YiffSpace::Search::QueryBuilder
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../search/query_dsl")
4
+
5
+ QueryDSL = YiffSpace::Search::QueryDSL
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../search/query_helper")
4
+
5
+ QueryHelper = YiffSpace::Search::QueryHelper
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/routes")
4
+
5
+ Routes = YiffSpace::Utils::Routes
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/table_builder")
4
+
5
+ TableBuilder = YiffSpace::Utils::TableBuilder
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/trace_logger")
4
+
5
+ TraceLogger = YiffSpace::Utils::TraceLogger
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../utils/user_attribute")
4
+
5
+ UserAttribute = YiffSpace::Utils::UserAttribute
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ class QueryBuilder
6
+ attr_reader(:dsl, :klass, :relation, :user, :q)
7
+
8
+ # [param, db_field, type]
9
+ def initialize(dsl, klass, relation, user)
10
+ dsl ||= []
11
+ @dsl = dsl
12
+ @klass = klass
13
+ @relation = relation
14
+ @user = user
15
+ @q = relation
16
+ end
17
+
18
+ def process_dsl(dsl, params)
19
+ dsl[:fields].each do |param, field, type = nil, block = nil, options = {}|
20
+ value = params[param]
21
+ multi = options.fetch(:multi, nil) || false
22
+ like = options.fetch(:like, nil) || false
23
+ ilike = options.fetch(:ilike, nil) || false
24
+ normalize = options.fetch(:normalize, nil) || ->(value) { value }
25
+ next if value.nil?
26
+
27
+ value = normalize.call(value)
28
+ @q = block.call(q) if block
29
+ if type.is_a?(Proc)
30
+ args = [q, value, user, params]
31
+ @q = type.call(*args.first(type.arity))
32
+ next
33
+ end
34
+ field ||= param
35
+ if like
36
+ @q = q.where.like(field => value)
37
+ next
38
+ elsif ilike
39
+ @q = q.where.ilike(field => value)
40
+ next
41
+ end
42
+ type ||= QueryHelper.get_column(field, klass.table_name).sql_type_metadata.type
43
+ case type
44
+ when :boolean
45
+ @q = q.boolean_attribute_matches(field, value)
46
+ when :integer
47
+ # multiple comma separated values are implicitly supported
48
+ # trimming them seems unneeded
49
+ # value = value[0.. value.index(",") - 1] unless multi
50
+ @q = q.numeric_attribute_matches(field, value)
51
+ when :datetime
52
+ @q = q.datetime_attribute_matches(field, value)
53
+ when :text, :string
54
+ value = value.split(",").first(YiffSpace.config.max_multi_count.call) if multi # explicitly supports arrays
55
+ @q = q.text_attribute_matches(field, value)
56
+ when :inet
57
+ @q = q.ip_attribute_matches(field, value)
58
+ when :present
59
+ @q = q.attribute_present(field, value)
60
+ else
61
+ raise(ArgumentError, "Unknown type: #{type} for field: #{field}")
62
+ end
63
+ end
64
+
65
+ dsl[:associations].each do |attribute, klass, nested_dsl, join|
66
+ next if params[attribute].nil?
67
+
68
+ @q = q.joins(join)
69
+ nested_relation = klass.all
70
+ nested_builder = QueryBuilder.new(nested_dsl, klass, nested_relation, user)
71
+ nested_relation = nested_builder.search(params[attribute])
72
+ @q = q.merge(nested_relation)
73
+ end
74
+ end
75
+
76
+ def search(params = {})
77
+ params.transform_keys!(&:to_sym)
78
+ process_dsl(dsl, params)
79
+ q
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ class QueryDSL
6
+ attr_accessor(:relation, :fields, :associations, :no_id, :no_dates)
7
+
8
+ # [param, field, type/proc, Proc(relation)]
9
+ def initialize(relation)
10
+ @relation = relation
11
+ @fields = []
12
+ @associations = []
13
+ @no_id = false
14
+ @no_dates = false
15
+ end
16
+
17
+ def custom(param, proc, not: false, multi: false, &block)
18
+ @fields << [param, nil, proc, block, { not: binding.local_variable_get(:not), multi: multi }]
19
+ self
20
+ end
21
+
22
+ def field(param, db_field = param, type = nil, not: false, multi: false, wildcard: false, like: false, ilike: false, normalize: nil, &block)
23
+ type ||= QueryHelper.get_column(db_field, relation.table_name).sql_type_metadata.type
24
+ @fields << [param, db_field, type, block, { not: binding.local_variable_get(:not), multi: multi, wildcard: wildcard, like: like, ilike: ilike, normalize: normalize }]
25
+ self
26
+ end
27
+
28
+ def user(param, association = nil, not: false, &)
29
+ if param.is_a?(Array)
30
+ raise(ArgumentError, "You must specify an association when passing an array of parameters") if association.nil?
31
+ else
32
+ association ||= param
33
+ param = [:"#{param}_id", :"#{param}_name"]
34
+ end
35
+
36
+ case association
37
+ when Symbol, String
38
+ association = association.to_sym
39
+ associations = relation.reflect_on_all_associations(:belongs_to).map(&:name)
40
+ if associations.include?(association)
41
+ column = association_column(association)
42
+ else
43
+ column = association
44
+ end
45
+ when Array
46
+ column = association.first
47
+ association = association.second
48
+ end
49
+ field(param.first, column, not: binding.local_variable_get(:not), &) if param.first
50
+ custom(param.second, ->(q, v) { q.user_name_matches(association, v) }, not: binding.local_variable_get(:not), &) if param.second
51
+ self
52
+ end
53
+
54
+ def present(param, db_field = param, &)
55
+ field(param, db_field, :present, &)
56
+ end
57
+
58
+ # [attribute, class, dsl, join]
59
+ def association(*args, **kwargs)
60
+ if args.any?
61
+ attribute = args.first
62
+ as = args.second || attribute
63
+ ref = relation.reflect_on_association(attribute)
64
+ raise("Association #{attribute} does not exist on #{relation.name}") if ref.nil?
65
+
66
+ klass = ref.klass
67
+ dsl = klass.query_dsl.build
68
+ @associations << [as, klass, dsl, attribute]
69
+ user_class = YiffSpace.config.user_class
70
+ user(as, attribute) if klass == user_class && @fields.none? { |f| f.second == association_column(attribute) }
71
+ elsif kwargs.any?
72
+ attribute = kwargs.delete(:as)
73
+ to_array = lambda { |h|
74
+ k, v = h.first
75
+ [k, v.is_a?(Hash) ? to_array.call(v) : v].flatten
76
+ }
77
+ through = to_array.call(kwargs)
78
+ attribute ||= through.last
79
+ klass = relation
80
+ through.each { |k| klass = klass.reflect_on_association(k).klass }
81
+ dsl = klass.query_dsl.build
82
+ join = through.reverse.reduce { |acc, key| { key => acc } }
83
+ @associations << [attribute, klass, dsl, join]
84
+ else
85
+ raise(ArgumentError, "Missing association")
86
+ end
87
+ self
88
+ end
89
+
90
+ def no_id!
91
+ @no_id = true
92
+ @fields.reject! { |field| field.first == :id }
93
+ self
94
+ end
95
+
96
+ def no_dates!
97
+ @no_dates = true
98
+ @fields.reject! { |field| %i[created_at updated_at].include?(field.first) }
99
+ self
100
+ end
101
+
102
+ def build
103
+ @fields << %i[id id integer] if @fields.none? { |field| field.first == :id }
104
+ @fields << %i[created_at created_at datetime] if @fields.none? { |field| field.first == :created_at } && relation.attribute_names.include?("created_at")
105
+ @fields << %i[updated_at updated_at datetime] if @fields.none? { |field| field.first == :updated_at } && relation.attribute_names.include?("updated_at")
106
+ {
107
+ fields: fields,
108
+ associations: associations,
109
+ }
110
+ end
111
+
112
+ private
113
+
114
+ def association_column(association)
115
+ :"#{relation.table_name}.#{relation.reflect_on_association(association).foreign_key}"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Search
5
+ module QueryHelper
6
+ module_function
7
+
8
+ def parse_conditions(conditions, model, table_name = nil)
9
+ pairs = []
10
+
11
+ conditions.each do |key, value|
12
+ case key
13
+ when String, Symbol
14
+ key = key.to_s
15
+ if value.is_a?(Hash)
16
+ pairs.concat(parse_conditions(value, model, key))
17
+ elsif key.include?(".")
18
+ table, col = key.split(".", 2)
19
+ pairs << [[table, col], value]
20
+ else
21
+ pairs << [[table_name || model.table_name, key], value]
22
+ end
23
+ else
24
+ raise(ArgumentError, "Unsupported key type: #{key.class}")
25
+ end
26
+ end
27
+
28
+ pairs
29
+ end
30
+
31
+ def get_column(attribute, table = nil)
32
+ attribute = attribute.to_s
33
+ if attribute.include?(".")
34
+ table, column = attribute.split(".", 2)
35
+ else
36
+ column = attribute
37
+ end
38
+ raise(ArgumentError, "Missing table") if table.nil?
39
+
40
+ c = ActiveRecord::Base.connection.columns(table).find { |c| c.name == column }
41
+ raise(StandardError, "Column #{column} does not exist in table #{table}") unless c
42
+
43
+ c
44
+ rescue ActiveRecord::StatementInvalid
45
+ raise(StandardError, "Table #{table} does not exist")
46
+ end
47
+ end
48
+ end
49
+ 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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ class Current < ActiveSupport::CurrentAttributes
6
+ attribute(:user, :ip_addr, :request)
7
+
8
+ def initialize
9
+ super
10
+ reset
11
+ end
12
+
13
+ after_reset do
14
+ attributes[:user] = YiffSpace.config.anonymous_user_getter.call
15
+ attributes[:ip_addr] = YiffSpace.config.default_ip_address
16
+ end
17
+
18
+ def user
19
+ value = super
20
+ return value if value.is_a?(YiffSpace.config.user_resolvable_class) || !value.is_a?(YiffSpace.config.user_class)
21
+ return YiffSpace.config.user_resolvable_class.new(value, ip_addr) if ip_addr.present?
22
+
23
+ value
24
+ end
25
+
26
+ def user=(value)
27
+ if value.is_a?(YiffSpace.config.user_resolvable_class)
28
+ self.ip_addr = value.ip_addr
29
+ value = value.user
30
+ end
31
+ super
32
+ end
33
+
34
+ def self.scoped(user, ip_addr = YiffSpace.config.default_ip_address, &)
35
+ set(user: user, ip_addr: ip_addr, &)
36
+ end
37
+
38
+ def self.as_system(&)
39
+ scoped(YiffSpace.config.system_user_getter.call, &)
40
+ end
41
+ end
42
+ end
43
+ 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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ module Helpers
6
+ module_function
7
+
8
+ def method_missing(name, *, &)
9
+ target.send(name, *, &)
10
+ end
11
+
12
+ def respond_to_missing?(...)
13
+ target.respond_to?(...)
14
+ end
15
+
16
+ private
17
+
18
+ # Lazily resolved so ApplicationController is not referenced at load time.
19
+ def target
20
+ ApplicationController.helpers
21
+ end
22
+ end
23
+ end
24
+ end