Class: LcpRuby::Dsl::ModelBuilder

Inherits:
Object
  • Object
show all
Includes:
SourceLocationCapture
Defined in:
lib/lcp_ruby/dsl/model_builder.rb

Constant Summary collapse

COLUMN_OPTION_KEYS =
%i[limit precision scale null].freeze

Instance Method Summary collapse

Methods included from SourceLocationCapture

capture_source_loc

Constructor Details

#initialize(name) ⇒ ModelBuilder

Returns a new instance of ModelBuilder.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 10

def initialize(name)
  @name = name.to_s
  @label = nil
  @label_plural = nil
  @label_source_loc = nil
  @label_plural_source_loc = nil
  @table_name_value = nil
  @fields = []
  @model_validations = []
  @associations = []
  @scopes = []
  @events = []
  @display_templates = {}
  @virtual_columns = {}
  @positioning_config = nil
  @data_source_config = nil
  @indexes = []
  @options = {}
  @abstract = false
  @bind_to_value = nil
  @bind_to_apply_value = []
  @slug_field_value = nil
end

Instance Method Details

#abstract(value = true) ⇒ Object



34
35
36
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 34

def abstract(value = true)
  @abstract = value
end

#abstract?Boolean

Returns:

  • (Boolean)


38
39
40
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 38

def abstract?
  @abstract
end

#after_create(name = nil) ⇒ Object

Lifecycle events



205
206
207
208
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 205

def after_create(name = nil)
  event_name = name&.to_s || "after_create"
  @events << { "name" => event_name }
end

#after_destroy(name = nil) ⇒ Object



220
221
222
223
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 220

def after_destroy(name = nil)
  event_name = name&.to_s || "after_destroy"
  @events << { "name" => event_name }
end

#after_update(name = nil) ⇒ Object



210
211
212
213
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 210

def after_update(name = nil)
  event_name = name&.to_s || "after_update"
  @events << { "name" => event_name }
end

#auditing(value = true, **opts) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 284

def auditing(value = true, **opts)
  hash = if value == true && opts.any?
    h = {}
    h["only"] = opts[:only].map(&:to_s) if opts[:only]
    h["ignore"] = opts[:ignore].map(&:to_s) if opts[:ignore]
    h["track_associations"] = opts[:track_associations] if opts.key?(:track_associations)
    h["track_attachments"] = opts[:track_attachments] if opts.key?(:track_attachments)
    h["expand_custom_fields"] = opts[:expand_custom_fields] if opts.key?(:expand_custom_fields)
    h["expand_json_fields"] = opts[:expand_json_fields].map(&:to_s) if opts[:expand_json_fields]
    h
  end
  set_boolean_or_hash_option("auditing", value, hash)
end

#before_destroy(name = nil) ⇒ Object



215
216
217
218
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 215

def before_destroy(name = nil)
  event_name = name&.to_s || "before_destroy"
  @events << { "name" => event_name }
end

#belongs_to(name, **options, &block) ⇒ Object

Raises:



176
177
178
179
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 176

def belongs_to(name, **options, &block)
  raise MetadataError, "belongs_to :#{name} does not accept a block — use `field :#{name}_id do ... end` for validations on the FK column" if block
  add_association("belongs_to", name, **options)
end

#bind_to(class_name) ⇒ Object



56
57
58
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 56

def bind_to(class_name)
  @bind_to_value = class_name.to_s
end

#bind_to_apply(*features) ⇒ Object



60
61
62
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 60

def bind_to_apply(*features)
  @bind_to_apply_value = features.map(&:to_s)
end

#custom_fields(value = true) ⇒ Object



80
81
82
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 80

def custom_fields(value = true)
  @options["custom_fields"] = value
end

#data_source(type:, **options) ⇒ Object



262
263
264
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 262

def data_source(type:, **options)
  @data_source_config = stringify_keys({ type: type }.merge(options))
end

#display_template(name, template: nil, subtitle: nil, icon: nil, badge: nil, renderer: nil, partial: nil, options: nil) ⇒ Object

Display templates



226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 226

def display_template(name, template: nil, subtitle: nil, icon: nil,
                     badge: nil, renderer: nil, partial: nil, options: nil)
  tmpl = {}
  tmpl["template"] = template if template
  tmpl["subtitle"] = subtitle if subtitle
  tmpl["icon"] = icon.to_s if icon
  tmpl["badge"] = badge if badge
  tmpl["renderer"] = renderer.to_s if renderer
  tmpl["partial"] = partial.to_s if partial
  tmpl["options"] = stringify_keys(options) if options
  @display_templates[name.to_s] = tmpl
end

#field(name, type, **options, &block) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 84

def field(name, type, **options, &block)
  # Capture once per `field` call. Enum value labels share this
  # location (per spec §I "Special case: enum value labels — source
  # line is the enclosing field declaration"). Pass 2 (AST) catches
  # the precise enum pair when finer granularity matters.
  field_source_loc = capture_source_loc

  field_hash = {
    "name" => name.to_s,
    "type" => type.to_s
  }
  field_hash["_source_loc"] = field_source_loc if field_source_loc
  field_hash["_label_source_loc"] = field_source_loc if field_source_loc && options.key?(:label)

  field_hash["item_type"] = options[:item_type].to_s if options.key?(:item_type)
  field_hash["label"] = options[:label] if options.key?(:label)
  field_hash["default"] = options[:default] if options.key?(:default)
  field_hash["transforms"] = options[:transforms].map(&:to_s) if options[:transforms]
  field_hash["computed"] = options[:computed] if options.key?(:computed)
  field_hash["sequence"] = stringify_keys(options[:sequence]) if options.key?(:sequence)
  field_hash["lcp_managed"] = options[:lcp_managed] if options.key?(:lcp_managed)

  if options.key?(:source)
    src = options[:source]
    field_hash["source"] =
      case src
      when :external, "external" then "external"
      when Hash then stringify_keys(src)
      else src.to_s
      end
  end

  # Extract column options from top-level kwargs
  column_opts = {}
  COLUMN_OPTION_KEYS.each do |key|
    column_opts[key.to_s] = options[key] if options.key?(key)
  end
  field_hash["column_options"] = column_opts unless column_opts.empty?

  # Handle enum values
  if options.key?(:values)
    field_hash["enum_values"] = normalize_enum_values(options[:values], field_source_loc)
  end

  # Handle attachment options
  if options.key?(:options)
    field_hash["options"] = stringify_keys(options[:options])
  end

  # Handle field-level validations via block
  if block
    field_builder = FieldBuilder.new
    field_builder.instance_eval(&block)
    field_hash["validations"] = field_builder.validations unless field_builder.validations.empty?
  end

  @fields << field_hash
end

#has_many(name, **options, &block) ⇒ Object

rubocop:disable Naming/PredicateName

Raises:



181
182
183
184
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 181

def has_many(name, **options, &block) # rubocop:disable Naming/PredicateName
  raise MetadataError, "has_many :#{name} does not accept a block" if block
  add_association("has_many", name, **options)
end

#has_one(name, **options, &block) ⇒ Object

rubocop:disable Naming/PredicateName

Raises:



186
187
188
189
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 186

def has_one(name, **options, &block) # rubocop:disable Naming/PredicateName
  raise MetadataError, "has_one :#{name} does not accept a block" if block
  add_association("has_one", name, **options)
end

#index(columns, unique: false, name: nil) ⇒ Object



266
267
268
269
270
271
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 266

def index(columns, unique: false, name: nil)
  idx = { "columns" => Array(columns).map(&:to_s) }
  idx["unique"] = true if unique
  idx["name"] = name.to_s if name
  @indexes << idx
end

#label(value) ⇒ Object



42
43
44
45
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 42

def label(value)
  @label = value
  @label_source_loc = capture_source_loc
end

#label_method(value) ⇒ Object



76
77
78
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 76

def label_method(value)
  @options["label_method"] = value.to_s
end

#label_plural(value) ⇒ Object



47
48
49
50
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 47

def label_plural(value)
  @label_plural = value
  @label_plural_source_loc = capture_source_loc
end

#on_field_change(name, field:, condition: nil) ⇒ Object

Field change events



335
336
337
338
339
340
341
342
343
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 335

def on_field_change(name, field:, condition: nil)
  event_hash = {
    "name" => name.to_s,
    "type" => "field_change",
    "field" => field.to_s
  }
  event_hash["condition"] = condition if condition
  @events << event_hash
end

#option(key, value) ⇒ Object



68
69
70
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 68

def option(key, value)
  @options[key.to_s] = value
end

#positioning(field: :position, scope: nil) ⇒ Object



273
274
275
276
277
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 273

def positioning(field: :position, scope: nil)
  config = { "field" => field.to_s }
  config["scope"] = Array(scope).map(&:to_s) if scope
  @positioning_config = config
end

#scope(name, **options) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 191

def scope(name, **options)
  scope_hash = { "name" => name.to_s }
  scope_hash["where"] = stringify_keys(options[:where]) if options.key?(:where)
  scope_hash["where_not"] = stringify_keys(options[:where_not]) if options.key?(:where_not)
  scope_hash["order"] = stringify_keys(options[:order]) if options.key?(:order)
  scope_hash["limit"] = options[:limit] if options.key?(:limit)
  scope_hash["type"] = options[:type].to_s if options.key?(:type)
  if options.key?(:parameters)
    scope_hash["parameters"] = options[:parameters].map { |p| stringify_keys(p) }
  end
  @scopes << scope_hash
end

#slug_field(name) ⇒ Object



64
65
66
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 64

def slug_field(name)
  @slug_field_value = name.to_s
end

#soft_delete(value = true, column: nil) ⇒ Object



279
280
281
282
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 279

def soft_delete(value = true, column: nil)
  hash = column ? { "column" => column.to_s } : nil
  set_boolean_or_hash_option("soft_delete", value, hash)
end

#table_name(value) ⇒ Object



52
53
54
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 52

def table_name(value)
  @table_name_value = value
end

#timestamps(value) ⇒ Object



72
73
74
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 72

def timestamps(value)
  @options["timestamps"] = value
end

#to_hashObject



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 345

def to_hash
  hash = { "name" => @name }
  hash["abstract"] = true if @abstract
  hash["label"] = @label if @label
  hash["_label_source_loc"] = @label_source_loc if @label_source_loc
  hash["label_plural"] = @label_plural if @label_plural
  hash["_label_plural_source_loc"] = @label_plural_source_loc if @label_plural_source_loc
  hash["table_name"] = @table_name_value if @table_name_value
  hash["bind_to"] = @bind_to_value if @bind_to_value
  hash["bind_to_apply"] = @bind_to_apply_value unless @bind_to_apply_value.empty?
  hash["slug_field"] = @slug_field_value if @slug_field_value

  fields_with_model_validations = merge_model_validations
  hash["fields"] = fields_with_model_validations unless fields_with_model_validations.empty?

  model_level_vals = extract_model_level_validations
  hash["validations"] = model_level_vals unless model_level_vals.empty?

  hash["associations"] = @associations unless @associations.empty?
  hash["scopes"] = @scopes unless @scopes.empty?
  hash["events"] = @events unless @events.empty?
  hash["display_templates"] = @display_templates unless @display_templates.empty?
  hash["virtual_columns"] = @virtual_columns unless @virtual_columns.empty?
  hash["positioning"] = @positioning_config if @positioning_config
  hash["data_source"] = @data_source_config if @data_source_config
  hash["indexes"] = @indexes unless @indexes.empty?
  hash["options"] = @options unless @options.empty?

  hash
end

#to_yamlObject



376
377
378
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 376

def to_yaml
  { "model" => to_hash }.to_yaml
end

#tree(value = true, parent_field: nil, children_name: nil, parent_name: nil, dependent: nil, max_depth: nil, ordered: nil, position_field: nil) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 318

def tree(value = true, parent_field: nil, children_name: nil, parent_name: nil,
         dependent: nil, max_depth: nil, ordered: nil, position_field: nil)
  hash = if value == true && (parent_field || children_name || parent_name || dependent || max_depth || !ordered.nil? || position_field)
    h = {}
    h["parent_field"] = parent_field.to_s if parent_field
    h["children_name"] = children_name.to_s if children_name
    h["parent_name"] = parent_name.to_s if parent_name
    h["dependent"] = dependent.to_s if dependent
    h["max_depth"] = max_depth if max_depth
    h["ordered"] = ordered unless ordered.nil?
    h["position_field"] = position_field.to_s if position_field
    h
  end
  set_boolean_or_hash_option("tree", value, hash)
end

#userstamps(value = true, created_by: nil, updated_by: nil, store_name: nil, created_by_association: nil, updated_by_association: nil, created_by_name_field: nil, updated_by_name_field: nil) ⇒ Object



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 298

def userstamps(value = true, created_by: nil, updated_by: nil, store_name: nil,
               created_by_association: nil, updated_by_association: nil,
               created_by_name_field: nil, updated_by_name_field: nil)
  all_opts = [ created_by, updated_by, store_name,
               created_by_association, updated_by_association,
               created_by_name_field, updated_by_name_field ]
  hash = if value == true && all_opts.any? { |o| !o.nil? }
    h = {}
    h["created_by"] = created_by.to_s if created_by
    h["updated_by"] = updated_by.to_s if updated_by
    h["store_name"] = store_name unless store_name.nil?
    h["created_by_association"] = created_by_association.to_s if created_by_association
    h["updated_by_association"] = updated_by_association.to_s if updated_by_association
    h["created_by_name_field"] = created_by_name_field.to_s if created_by_name_field
    h["updated_by_name_field"] = updated_by_name_field.to_s if updated_by_name_field
    h
  end
  set_boolean_or_hash_option("userstamps", value, hash)
end

#validates(field_name, type, **options) ⇒ Object

Style B: model-level validates :field_name, :type, **options



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 144

def validates(field_name, type, **options)
  when_condition = options.delete(:when)
  field_ref = options.delete(:field_ref)
  operator = options.delete(:operator)
  message = options.delete(:message)
  service = options.delete(:service)

  @model_validations << {
    field: field_name.to_s,
    type: type.to_s,
    options: options,
    when: when_condition,
    field_ref: field_ref,
    operator: operator,
    message: message,
    service: service
  }
end

#validates_model(type, **options) ⇒ Object

Model-level validations not attached to a field



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 164

def validates_model(type, **options)
  validation = { "type" => type.to_s }
  validator_class = options.delete(:validator_class)
  validation["validator_class"] = validator_class if validator_class
  when_condition = options.delete(:when)
  validation["when"] = when_condition if when_condition
  service = options.delete(:service)
  validation["service"] = service.to_s if service
  validation["options"] = stringify_keys(options) unless options.empty?
  @model_validations << { model_level: true, hash: validation }
end

#virtual_column(name, **opts) ⇒ Object Also known as: aggregate



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/lcp_ruby/dsl/model_builder.rb', line 239

def virtual_column(name, **opts)
  vc = {}
  vc["function"] = opts[:function].to_s if opts[:function]
  vc["association"] = opts[:association].to_s if opts[:association]
  vc["source_field"] = opts[:source_field].to_s if opts[:source_field]
  vc["where"] = stringify_keys(opts[:where]) if opts[:where]
  vc["distinct"] = opts[:distinct] if opts.key?(:distinct)
  vc["default"] = opts[:default] if opts.key?(:default)
  vc["include_discarded"] = opts[:include_discarded] if opts.key?(:include_discarded)
  vc["expression"] = opts[:expression].to_s if opts[:expression]
  vc["sql"] = opts[:sql].to_s if opts[:sql]
  vc["service"] = opts[:service].to_s if opts[:service]
  vc["type"] = opts[:type].to_s if opts[:type]
  vc["options"] = stringify_keys(opts[:options]) if opts[:options]
  vc["join"] = opts[:join].to_s if opts[:join]
  vc["group"] = opts[:group] if opts.key?(:group)
  vc["auto_include"] = opts[:auto_include] if opts.key?(:auto_include)
  @virtual_columns[name.to_s] = vc
end