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,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YiffSpace
4
+ module Utils
5
+ # User class must have id_to_name & name_to_id methods, and global u2id method must exist
6
+ class UserAttribute
7
+ class CircularAliasError < StandardError; end
8
+ class CircularCloneError < StandardError; end
9
+ class DuplicateAttributeError < StandardError; end
10
+ class CloneError < StandardError; end
11
+ attr_reader(:klass, :attribute, :db, :ip, :optional, :clones, :overwrite, :aliases, :ignore_nil, :ar_options)
12
+
13
+ def initialize(klass, attribute, db:, ip: !db, optional: !db, clones: [], overwrite: false, aliases: [], ignore_nil: false, **ar_options)
14
+ UserAttribute.class_setup(klass)
15
+ @klass = klass
16
+ @attribute = attribute.to_sym
17
+ @db = db
18
+ # noinspection RubySimplifyBooleanInspection
19
+ @ip = ip == true ? :"#{attribute}_ip_addr" : ip
20
+ @optional = optional
21
+ @clones = Array(clones).map(&:to_sym)
22
+ @overwrite = overwrite
23
+ @aliases = Array(aliases).map(&:to_sym)
24
+ @ignore_nil = ignore_nil
25
+ @ar_options = ar_options
26
+ if ar_options.any? && !db # rubocop:disable Style/IfUnlessModifier
27
+ TraceLogger.warn("UserAttribute", "Unexpected extra options for non-db attribute: #{ar_options.inspect}", ignore: [%r{/concerns/user_methods\.rb}, %r{/logical/user_attribute\.rb}])
28
+ end
29
+ @ar_options[:optional] = optional
30
+ @ar_options[:class_name] = "User"
31
+ validate_options!
32
+ klass.user_attributes[attribute] = self
33
+ define_methods
34
+ end
35
+
36
+ def validate_options!
37
+ raise(DuplicateAttributeError, "user attribute #{attribute} is already defined on #{klass}") if klass.user_attributes.key?(attribute)
38
+
39
+ validate_aliases!
40
+ validate_clones!
41
+ end
42
+
43
+ def validate_aliases!
44
+ return if aliases.empty?
45
+ raise(CircularAliasError, "aliases must not contain attribute (#{klass}.#{attribute}") if aliases.include?(attribute)
46
+ end
47
+
48
+ def validate_clones!
49
+ return if clones.empty?
50
+ raise(CircularCloneError, "clones must not contain attribute (#{klass}.#{attribute}") if clones.include?(attribute)
51
+ # TODO: implement clone validation logic
52
+ end
53
+
54
+ def validate_value!(value, type, record: nil)
55
+ return if value.nil?
56
+
57
+ case type
58
+ when :user
59
+ return if (optional || ignore_nil) && value.nil?
60
+ return if value.is_a?(::User)
61
+
62
+ if value.is_a?(::UserResolvable)
63
+ return if value.user.is_a?(::User)
64
+
65
+ raise(ArgumentError, "Expected User for UserResolvable.user in #{klass.name}##{attribute}, got #{value.user.inspect} (#{value.user.class.name})")
66
+ end
67
+ value_error("is invalid", "UserLike", attribute, value, record: record)
68
+ when :id
69
+ value_error("is invalid", "Integer", "#{attribute}_id", value, record: record) unless value.is_a?(Integer) || (value.is_a?(String) && /\A\d+\Z/.match?(value))
70
+ when :name
71
+ value_error("is invalid", "String", "#{attribute}_name", value, record: record) unless value.is_a?(String)
72
+ when :ip_addr
73
+ return if value.is_a?(IPAddr)
74
+
75
+ begin
76
+ IPAddr.new(value)
77
+ rescue IPAddr::InvalidAddressError
78
+ value_error("is invalid", "IPAddr", ip, value, record: record)
79
+ end
80
+ else
81
+ raise(ArgumentError, "Not sure how to validate #{type}")
82
+ end
83
+ end
84
+
85
+ def value_error(message, expected, attr, value, record: nil)
86
+ raise(ArgumentError, "Expected #{expected} for #{klass.name}.#{attr}=, got #{value.inspect} (#{value.class.name})") unless record.respond_to?(:errors) && record.errors.is_a?(ActiveModel::Errors)
87
+
88
+ record.errors.add(attr, message)
89
+ end
90
+
91
+ def define_methods # rubocop:disable Metrics/MethodLength
92
+ ua = self
93
+ klass.define_singleton_method(:"#{attribute}_attribute") { ua }
94
+ klass.belongs_to(attribute, **ar_options) if db
95
+ if (klass < ActiveModel::Validations) && ip && !optional
96
+ # klass.validates(attribute, presence: { message: "must exist" }) unless optional
97
+ klass.validates(ip, presence: true, if: -> { send("#{ua.attribute}_id").present? })
98
+ end
99
+ klass.define_method(attribute) do
100
+ if ua.db
101
+ value = super()
102
+ else
103
+ value = instance_variable_get(:"@#{ua.attribute}")
104
+ value = ua.get_clone_value(self) if value.blank?
105
+ end
106
+ return if value.blank?
107
+
108
+ if ua.ip && (ip_addr = send(ua.ip))
109
+ ::UserResolvable.new(value, ip_addr)
110
+ else
111
+ value
112
+ end
113
+ end
114
+ klass.define_method("#{attribute}=") do |value|
115
+ ua.validate_value!(value, :user, record: self)
116
+ if ua.clones.any?
117
+ ua.clones.each do |cattr|
118
+ ua.clone_value(self, cattr, value)
119
+ end
120
+ end
121
+
122
+ if value.is_a?(::UserResolvable)
123
+ send("#{ua.ip}=", value.ip_addr) if ua.ip
124
+ value = value.user
125
+ end
126
+
127
+ if ua.db
128
+ super(value)
129
+ else
130
+ instance_variable_set(:"@#{ua.attribute}", value)
131
+ end
132
+ end
133
+ klass.define_method("#{attribute}_id") do
134
+ if ua.db
135
+ read_attribute("#{ua.attribute}_id")
136
+ else
137
+ value = instance_variable_get(:"@#{ua.attribute}")&.id
138
+ value = ua.get_clone_value(self, :id) if value.blank?
139
+ value
140
+ end
141
+ end
142
+ klass.define_method("#{attribute}_id=") do |value|
143
+ ua.validate_value!(value, :id, record: self)
144
+ if ua.db
145
+ write_attribute("#{ua.attribute}_id", value)
146
+ TraceLogger.warn("UserAttribute", "#{ua.klass.name}.#{ua.attribute}_id= should not be used when clone is enabled (#{ua.clones.join(', ')})", ignore: [%r{/concerns/user_methods\.rb}, %r{/logical/user_attribute\.rb}]) if ua.clones.any?
147
+ else
148
+ value = ::User.find(value) if value.is_a?(Integer) || value.is_a?(String)
149
+ ua.clones.each do |cattr|
150
+ ua.clone_direct(self, cattr, value)
151
+ end
152
+ instance_variable_set(:"@#{ua.attribute}", value)
153
+ end
154
+ end
155
+ klass.define_method("#{attribute}_name") do
156
+ value = send("#{ua.attribute}_id")
157
+ return "Anonymous" if value.blank?
158
+
159
+ ::User.id_to_name(value)
160
+ end
161
+ klass.define_method("#{attribute}_name=") do |value|
162
+ ua.validate_value!(value, :name, record: self)
163
+ value = ::User.name_to_id(value)
164
+ return send("#{ua.attribute}_id=", value) if value.blank?
165
+
166
+ send("#{ua.attribute}=", ::User.find(value))
167
+ end
168
+ if ip
169
+ klass.define_method(ip) do
170
+ if ua.db
171
+ read_attribute(ua.ip)
172
+ else
173
+ value = instance_variable_get(:"@#{ua.ip}")
174
+ value = ua.get_clone_value(self, :ip_addr) if value.blank?
175
+ value
176
+ end
177
+ end
178
+ klass.define_method("#{ip}=") do |value|
179
+ ua.validate_value!(value, :ip_addr, record: self)
180
+ if ua.db
181
+ write_attribute(ua.ip, value)
182
+ else
183
+ value = value.to_s.strip if value.is_a?(String)
184
+ ua.clones.each do |cattr|
185
+ ua.clone_ip(self, cattr, value)
186
+ end
187
+ instance_variable_set(:"@#{ua.ip}", value)
188
+ end
189
+ end
190
+ end
191
+ klass.instance_exec do
192
+ scope("for_#{ua.attribute}", ->(value) { where(ua.db && ua.ar_options.key?(:foreign_key) ? ua.ar_options[:foreign_key] : "#{ua.attribute}_id" => u2id(value)) }) unless respond_to?("for_#{ua.attribute}")
193
+ scope("for_#{ua.attribute}_id", ->(value) { where(ua.db && ua.ar_options.key?(:foreign_key) ? ua.ar_options[:foreign_key] : "#{ua.attribute}_id" => value) }) unless respond_to?("for_#{ua.attribute}_id")
194
+ scope("for_#{ua.attribute}_name", ->(value) { where(ua.db && ua.ar_options.key?(:foreign_key) ? ua.ar_options[:foreign_key] : "#{ua.attribute}_id" => ::User.name_to_id(value)) }) unless respond_to?("for_#{ua.attribute}_name")
195
+ end
196
+
197
+ aliases.each do |alias_attr|
198
+ create_alias(alias_attr)
199
+ end
200
+ end
201
+
202
+ def get_clone_value(record, type = nil)
203
+ attrs = klass.user_attributes.select { |_attr, options| options.clones.include?(attribute) }
204
+ value = nil
205
+ attrs.each_key do |cattr|
206
+ case type
207
+ when :id
208
+ value = record.send("#{cattr}_id")
209
+ when :ip_addr
210
+ value = record.send(attrs[cattr].ip) if attrs[cattr].ip
211
+ else
212
+ value = record.send(cattr)
213
+ end
214
+ break if value.present?
215
+ end
216
+ value
217
+ end
218
+
219
+ def clone_value(record, attr, value)
220
+ return record.send("#{attr}=", value) if overwrite
221
+
222
+ if record.respond_to?(:"#{attr}_id")
223
+ record.send(:"#{attr}=", value) if record.send(:"#{attr}_id").blank?
224
+ elsif record.respond_to?(attr)
225
+ record.send(:"#{attr}=", value) if record.send(attr).blank?
226
+ else
227
+ raise(CloneError, "Not sure how to clone to #{attr}, #{record.class} does not respond_to? #{attr}_id or #{attr}")
228
+ end
229
+ end
230
+
231
+ def clone_ip(record, attr, value)
232
+ ip_attr = klass.user_attributes.find { |uattr, _value| uattr == attr }&.second&.ip
233
+ raise(CloneError, "Not sure how to clone ip for #{attr}, no ip attribute defined") unless ip_attr
234
+
235
+ clone_direct(record, ip_attr, value)
236
+ end
237
+
238
+ def clone_direct(record, attr, value)
239
+ return record.send("#{attr}=", value) if overwrite
240
+
241
+ raise(CloneError, "Not sure how to clone to #{attr}, #{record.class} does not respond_to? #{attr}") unless record.respond_to?(attr)
242
+
243
+ record.send(:"#{attr}=", value) if record.send(attr).blank?
244
+ end
245
+
246
+ def create_alias(attr)
247
+ ua = self
248
+ klass.class_eval do
249
+ define_method(attr) { send(ua.attribute) }
250
+ define_method("#{attr}=") { |value| send("#{ua.attribute}=", value) }
251
+ define_method("#{attr}_id") { send("#{ua.attribute}_id") }
252
+ define_method("#{attr}_id=") { |value| send("#{ua.attribute}_id=", value) }
253
+ define_method("#{attr}_name") { send("#{ua.attribute}_name") }
254
+ define_method("#{attr}_name=") { |value| send("#{ua.attribute}_name=", value) }
255
+ if ua.ip
256
+ define_method("#{attr}_ip_addr") { send(ua.ip) }
257
+ define_method("#{attr}_ip_addr=") { |value| send("#{ua.ip}=", value) }
258
+ end
259
+ end
260
+ end
261
+
262
+ def self.class_setup(klass)
263
+ return if klass.method_defined?(:user_attributes)
264
+
265
+ klass.class_eval do
266
+ class_attribute(:user_attributes, default: {})
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YiffSpace
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end
data/lib/yiffspace.rb CHANGED
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YiffSpace
4
+ class << self
5
+ def config
6
+ @config ||= Configuration.new
7
+ end
8
+
9
+ def configure
10
+ yield(config)
11
+ end
12
+ end
4
13
  end
5
14
 
6
15
  require("zeitwerk")
7
16
 
8
17
  loader = Zeitwerk::Loader.for_gem
9
- loader.inflector.inflect({ "postgresql" => "PostgreSQL", "yiffspace" => "YiffSpace" })
18
+ loader.inflector.inflect({ "postgresql" => "PostgreSQL", "yiffspace" => "YiffSpace", "query_dsl" => "QueryDSL" })
10
19
  loader.ignore("#{__dir__}/yiffspace/core_ext")
20
+ loader.ignore("#{__dir__}/yiffspace/include")
11
21
  loader.setup
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yiffspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donovan_DMC
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-05-07 00:00:00.000000000 Z
10
+ date: 2026-05-28 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: abbrev
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.2
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: httparty
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -51,6 +65,20 @@ dependencies:
51
65
  - - ">="
52
66
  - !ruby/object:Gem::Version
53
67
  version: '7.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: request_store
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.7'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1.7'
54
82
  - !ruby/object:Gem::Dependency
55
83
  name: zeitwerk
56
84
  requirement: !ruby/object:Gem::Requirement
@@ -101,6 +129,16 @@ files:
101
129
  - lib/yiffspace/auth/set_client_name.rb
102
130
  - lib/yiffspace/auth/user_info.rb
103
131
  - lib/yiffspace/auth/user_info/anonymous.rb
132
+ - lib/yiffspace/concerns/active_record_extensions.rb
133
+ - lib/yiffspace/concerns/api_methods.rb
134
+ - lib/yiffspace/concerns/attribute_matchers.rb
135
+ - lib/yiffspace/concerns/attribute_methods.rb
136
+ - lib/yiffspace/concerns/concurrency_methods.rb
137
+ - lib/yiffspace/concerns/conditional_includes.rb
138
+ - lib/yiffspace/concerns/current_methods.rb
139
+ - lib/yiffspace/concerns/has_bit_flags.rb
140
+ - lib/yiffspace/concerns/user_methods.rb
141
+ - lib/yiffspace/concerns/user_name_methods.rb
104
142
  - lib/yiffspace/config_builder.rb
105
143
  - lib/yiffspace/configuration.rb
106
144
  - lib/yiffspace/configuration/images.rb
@@ -116,7 +154,6 @@ files:
116
154
  - lib/yiffspace/core_ext/enumerable/parallel.rb
117
155
  - lib/yiffspace/core_ext/hash/all.rb
118
156
  - lib/yiffspace/core_ext/hash/to_open_hash.rb
119
- - lib/yiffspace/core_ext/open_hash.rb
120
157
  - lib/yiffspace/core_ext/string/all.rb
121
158
  - lib/yiffspace/core_ext/string/sql.rb
122
159
  - lib/yiffspace/core_ext/string/to_b.rb
@@ -143,9 +180,37 @@ files:
143
180
  - lib/yiffspace/images/banner.rb
144
181
  - lib/yiffspace/images/banner/base.rb
145
182
  - lib/yiffspace/images/banner/discord.rb
183
+ - lib/yiffspace/include/all.rb
184
+ - lib/yiffspace/include/cache.rb
185
+ - lib/yiffspace/include/current.rb
186
+ - lib/yiffspace/include/duration_parser.rb
187
+ - lib/yiffspace/include/helpers.rb
188
+ - lib/yiffspace/include/open_hash.rb
189
+ - lib/yiffspace/include/parameter_builder.rb
190
+ - lib/yiffspace/include/parse_value.rb
191
+ - lib/yiffspace/include/query_builder.rb
192
+ - lib/yiffspace/include/query_dsl.rb
193
+ - lib/yiffspace/include/query_helper.rb
194
+ - lib/yiffspace/include/routes.rb
195
+ - lib/yiffspace/include/table_builder.rb
196
+ - lib/yiffspace/include/trace_logger.rb
197
+ - lib/yiffspace/include/user_attribute.rb
198
+ - lib/yiffspace/search/query_builder.rb
199
+ - lib/yiffspace/search/query_dsl.rb
200
+ - lib/yiffspace/search/query_helper.rb
146
201
  - lib/yiffspace/utils.rb
202
+ - lib/yiffspace/utils/cache.rb
203
+ - lib/yiffspace/utils/current.rb
204
+ - lib/yiffspace/utils/duration_parser.rb
205
+ - lib/yiffspace/utils/helpers.rb
147
206
  - lib/yiffspace/utils/open_hash.rb
207
+ - lib/yiffspace/utils/parameter_builder.rb
208
+ - lib/yiffspace/utils/parse_value.rb
209
+ - lib/yiffspace/utils/routes.rb
148
210
  - lib/yiffspace/utils/set_env_constraint.rb
211
+ - lib/yiffspace/utils/table_builder.rb
212
+ - lib/yiffspace/utils/trace_logger.rb
213
+ - lib/yiffspace/utils/user_attribute.rb
149
214
  - lib/yiffspace/version.rb
150
215
  homepage: https://yiff.space
151
216
  licenses: