Module: NoFlyList::TaggableRecord::Configuration

Defined in:
lib/no_fly_list/taggable_record/configuration.rb

Overview

Configuration module handles the setup and structure of tagging functionality This includes creating necessary classes, setting up associations, and defining the interface for tag manipulation

Class Method Summary collapse

Class Method Details

.build_tag_setup(taggable_klass, context, options) ⇒ Object

Creates a new TagSetup instance with the given configuration



30
31
32
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 30

def build_tag_setup(taggable_klass, context, options)
  TagSetup.new(taggable_klass, context, options)
end

.create_tag_class(setup, base_class) ⇒ Object

Creates a new tag class with appropriate configuration



76
77
78
79
80
81
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 76

def create_tag_class(setup, base_class)
  Class.new(base_class) do
    self.table_name = "#{setup.taggable_klass.table_name.singularize}_tags"
    include NoFlyList::TagRecord
  end
end

.create_tagging_class(setup, base_class) ⇒ Object

Creates a new tagging class with appropriate configuration



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 84

def create_tagging_class(setup, base_class)
  setup.context.to_s.singularize

  Class.new(base_class) do
    self.table_name = "#{setup.taggable_klass.table_name.singularize}_taggings"

    # Add the basic associations
    belongs_to :tag,
               class_name: setup.tag_class_name,
               foreign_key: "tag_id"

    belongs_to :taggable,
               class_name: setup.taggable_klass.name,
               foreign_key: "taggable_id"

    include NoFlyList::TaggingRecord
  end
end

.define_constant_in_namespace(const_name) ⇒ Object



300
301
302
303
304
305
306
307
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 300

def define_constant_in_namespace(const_name)
  parts = const_name.split("::")
  const_name = parts.pop
  namespace = parts.join("::").safe_constantize || Object
  return if namespace.const_defined?(const_name, false)

  namespace.const_set(const_name, yield)
end

.define_list_methods(setup) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 231

def define_list_methods(setup)
  context = setup.context
  taggable_klass = setup.taggable_klass

  # Define helper methods module for this context
  helper_module = Module.new do
    define_method :create_and_set_proxy do |instance_variable_name, setup|
      tag_model = if setup.polymorphic
                    setup.tag_class_name.constantize
      else
                    self.class.const_get("#{self.class.name}Tag")
      end

      proxy = TaggingProxy.new(
        self,
        tag_model,
        setup.context,
        transformer: setup.transformer,
        restrict_to_existing: setup.restrict_to_existing,
        limit: calculate_limit(setup.limit)
      )
      instance_variable_set(instance_variable_name, proxy)
    end

    define_method :calculate_limit do |limit|
      limit.is_a?(Proc) ? limit.call(self) : limit
    end

    define_method :reset_proxy_for do |context|
      instance_variable_name = "@_#{context}_list_proxy"
      remove_instance_variable(instance_variable_name) if instance_variable_defined?(instance_variable_name)
    end
  end

  # Include the helper methods
  taggable_klass.include(helper_module)

  # Define the public interface methods
  taggable_klass.class_eval do
    define_method "#{context}_list" do
      instance_variable_name = "@_#{context}_list_proxy"

      if instance_variable_defined?(instance_variable_name)
        instance_variable_get(instance_variable_name)
      else
        create_and_set_proxy(instance_variable_name, setup)
      end
    end

    define_method "#{context}_list=" do |tag_list|
      reset_proxy_for(context)
      proxy = send("#{context}_list")
      proxy.send("#{context}_list=", tag_list)
    end

    define_method "reset_#{context}_list" do
      reset_proxy_for(context)
    end
  end
end

.define_tag_classes(setup) ⇒ Object

Creates the tag and tagging classes for local (non-global) tags



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 63

def define_tag_classes(setup)
  base_class = find_abstract_class(setup.taggable_klass)

  define_constant_in_namespace(setup.tag_class_name) do
    create_tag_class(setup, base_class)
  end

  define_constant_in_namespace(setup.tagging_class_name) do
    create_tagging_class(setup, base_class)
  end
end

.define_tag_structure(setup) ⇒ Object

Sets up the complete tag structure including classes and associations



57
58
59
60
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 57

def define_tag_structure(setup)
  define_tag_classes(setup) unless setup.polymorphic
  define_tagging_associations(setup)
end

.define_tagging_associations(setup) ⇒ Object

Sets up all necessary associations between tags, taggings, and the taggable model



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 104

def define_tagging_associations(setup)
  singular_name = setup.context.to_s.singularize

  if setup.polymorphic
    setup_polymorphic_tag_associations(setup, singular_name)
  else
    setup_local_tag_associations(setup, singular_name)
  end

  setup_taggable_associations(setup, singular_name)
end

.determine_tag_class_name(taggable_klass, options) ⇒ Object

Determines the appropriate class name for tags based on configuration For global tags, uses application-wide tag class For local tags, creates model-specific tag classes



37
38
39
40
41
42
43
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 37

def determine_tag_class_name(taggable_klass, options)
  if options[:polymorphic]
    Rails.application.config.no_fly_list.tag_class_name
  else
    options.fetch(:tag_class_name, "#{taggable_klass.name}Tag")
  end
end

.determine_tagging_class_name(taggable_klass, options) ⇒ Object

Determines the appropriate class name for taggings based on configuration For global tags, uses application-wide tagging class For local tags, creates model-specific tagging classes



48
49
50
51
52
53
54
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 48

def determine_tagging_class_name(taggable_klass, options)
  if options[:polymorphic]
    Rails.application.config.no_fly_list.tagging_class_name
  else
    options.fetch(:tagging_class_name, "#{taggable_klass.name}::Tagging")
  end
end

.find_abstract_class(klass) ⇒ Object



292
293
294
295
296
297
298
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 292

def find_abstract_class(klass)
  while klass && !klass.abstract_class?
    klass = klass.superclass
    break if klass == ActiveRecord::Base || klass.nil?
  end
  klass || ActiveRecord::Base
end

.setup_local_tag_associations(setup, singular_name) ⇒ Object

Sets up associations for local (non-global) tags



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 158

def setup_local_tag_associations(setup, singular_name)
  # Set up tag class associations
  setup.tag_class_name.constantize.class_eval do
    has_many :"#{singular_name}_taggings",
             -> { where(context: singular_name) },
             class_name: setup.tagging_class_name,
             foreign_key: "tag_id",
             dependent: :destroy

    has_many :"#{singular_name}_taggables",
             through: :"#{singular_name}_taggings",
             source: :taggable
  end

  # Set up tagging class associations
  setup.tagging_class_name.constantize.class_eval do
    belongs_to :tag,
               class_name: setup.tag_class_name,
               foreign_key: "tag_id"

    # For local tags, we use a simple belongs_to without polymorphic
    belongs_to :taggable,
               class_name: setup.taggable_klass.name,
               foreign_key: "taggable_id"

    validates :tag, :taggable, :context, presence: true
    validates :tag_id, uniqueness: {
      scope: %i[taggable_id context],
      message: "has already been tagged on this record in this context"
    }
  end
end

.setup_polymorphic_tag_associations(setup, singular_name) ⇒ Object

Sets up associations for polymorphic tags



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 117

def setup_polymorphic_tag_associations(setup, singular_name)
  # Set up the tag model associations
  setup.tag_class_name.constantize.class_eval do
    # Fix: Use 'tagging' for the join association when context is 'tag'
    association_name = (singular_name == "tag" ? :taggings : :"#{singular_name}_taggings")

    has_many association_name,
             -> { where(context: singular_name) },
             class_name: setup.tagging_class_name,
             foreign_key: "tag_id",
             dependent: :destroy

    # Fix: Use consistent naming for through association
    has_many setup.context,
             through: association_name,
             source: :taggable,
             source_type: setup.taggable_klass.name
  end

  # Set up the tagging model with global scope
  setup.tagging_class_name.constantize.class_eval do
    belongs_to :tag,
               class_name: setup.tag_class_name,
               foreign_key: "tag_id"

    belongs_to :taggable,
               polymorphic: true

    validates :tag, :taggable, :context, presence: true

    # Add scope for specific taggable type
    scope :for_taggable_type, ->(type) { where(taggable_type: type) }

    validates :tag_id, uniqueness: {
      scope: %i[taggable_type taggable_id context],
      message: "has already been tagged on this record in this context"
    }
  end
end

.setup_taggable_associations(setup, singular_name) ⇒ Object

Sets up associations on the taggable model



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 192

def setup_taggable_associations(setup, singular_name)
  setup.taggable_klass.class_eval do
    if setup.polymorphic
      # Global tags need polymorphic associations
      has_many :"#{singular_name}_taggings",
               -> { where(context: singular_name) },
               class_name: setup.tagging_class_name,
               foreign_key: "taggable_id",
               as: :taggable,
               dependent: :destroy

      has_many setup.context,
               through: :"#{singular_name}_taggings",
               source: :tag,
               class_name: setup.tag_class_name do
        def by_type(taggable_type)
          where(taggings: { taggable_type: taggable_type })
        end

        def shared_with(other_taggable)
          where(id: other_taggable.send(proxy_association.name).pluck(:id))
        end
      end
    else
      # Local tags should use simple associations
      has_many :"#{singular_name}_taggings",
               -> { where(context: singular_name) },
               class_name: setup.tagging_class_name,
               foreign_key: "taggable_id",
               dependent: :destroy

      has_many setup.context,
               through: :"#{singular_name}_taggings",
               source: :tag,
               class_name: setup.tag_class_name
    end
  end
end

.setup_tagging(taggable_klass, contexts, options = {}) ⇒ Object

Main entry point for setting up tagging functionality on a model

Parameters:

  • taggable_klass (Class)

    The model class to make taggable

  • contexts (Array<Symbol>)

    The contexts to create tags for (e.g., :tags, :colors)

  • options (Hash) (defaults to: {})

    Configuration options for tagging behavior



19
20
21
22
23
24
25
26
27
# File 'lib/no_fly_list/taggable_record/configuration.rb', line 19

def setup_tagging(taggable_klass, contexts, options = {})
  contexts.each do |context|
    setup = build_tag_setup(taggable_klass, context, options)
    define_tag_structure(setup)
    define_list_methods(setup)
    Mutation.define_mutation_methods(setup)
    Query.define_query_methods(setup)
  end
end