Class: Exwiw::MongoidSchemaGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/exwiw/mongoid_schema_generator.rb

Overview

Generates exwiw MongodbCollectionConfig files by introspecting Mongoid document models. This is the MongoDB/Mongoid counterpart of SchemaGenerator (which targets ActiveRecord); it is intentionally a separate class and rake task because the two ORMs expose entirely different metadata APIs.

Introspection relies only on class-level Mongoid metadata (fields, relations, collection_name), so it does not require a live MongoDB connection.

Defined Under Namespace

Classes: UnsupportedEmbedding

Constant Summary collapse

PRIMARY_KEY =

Every generated collection is keyed by the MongoDB document id. Named so the value the configs carry and the value DefaultMask interpolates into a text mask (masked-{_id}) provably come from one place.

"_id"
MASKABLE_FIELD_TYPES =

Mongoid field types (Model.fields[name].type, a Ruby class) mapped to the type symbols DefaultMask.for understands, so safe mode can reuse the very same default masks the ActiveRecord generator emits.

Keyed by class NAME rather than by the class itself: this constant is evaluated when exwiw is loaded, which happens in processes that never load Mongoid (the CLI), so naming Mongoid::Boolean here would raise. A name comparison also sidesteps DateTime < Date — an ancestry test would have to order its branches, while exact names cannot be confused for one another.

Everything absent from this map (Hash, Array, Object — which is also what a typeless/dynamic field reports — BSON types, Mongoid::StringifiedSymbol, ...) deliberately gets NO default mask: a constant that does not fit the field would be restored as a value the application cannot read, which is worse than exporting the field while its needs_mask_decision flag keeps the change from being merged.

{
  "String" => :string,
  "Integer" => :integer,
  "Float" => :float,
  "BigDecimal" => :decimal,
  "Mongoid::Boolean" => :boolean,
  "Time" => :datetime,
  "DateTime" => :datetime,
  "ActiveSupport::TimeWithZone" => :datetime,
  "Date" => :date,
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(models:, output_dir:, skip_unsupported: false, safe_new_columns: true) ⇒ MongoidSchemaGenerator

safe_new_columns (the default) emits every field masked — as far as its Mongoid type allows, see MASKABLE_FIELD_TYPES and DefaultMask — and flagged needs_mask_decision: true. MongodbCollectionConfig#merge lets an existing entry win, so in practice only fields a model change has just added keep that treatment. Pass false to bootstrap a config, where every field is new and flagging all of them at once is noise.



91
92
93
94
95
96
# File 'lib/exwiw/mongoid_schema_generator.rb', line 91

def initialize(models:, output_dir:, skip_unsupported: false, safe_new_columns: true)
  @models = models
  @output_dir = output_dir
  @skip_unsupported = skip_unsupported
  @safe_new_columns = safe_new_columns
end

Class Method Details

.from_rails_application(output_dir:, skip_unsupported: false, safe_new_columns: true) ⇒ Object

skip_unsupported: when true, the generator does not abort on a construct it cannot represent. It skips an unresolvable belongs_to (keeping the foreign-key field) and emits an unrepresentable embedded collection as an ignore: true top-level config annotated with a comment, warning to stderr in both cases. Off by default, so the historical fail-loud behavior is unchanged unless a caller opts in.

safe_new_columns mirrors SchemaGenerator; see #initialize.



75
76
77
78
79
80
81
82
83
# File 'lib/exwiw/mongoid_schema_generator.rb', line 75

def self.from_rails_application(output_dir:, skip_unsupported: false, safe_new_columns: true)
  Rails.application.eager_load!
  new(
    models: ::Mongoid.models,
    output_dir: output_dir,
    skip_unsupported: skip_unsupported,
    safe_new_columns: safe_new_columns,
  )
end

Instance Method Details

#build_collections(existing_by_name = {}) ⇒ Object

Returns an array of MongodbCollectionConfig, one per collection (top-level collections and embedded subdocument configs alike).

Models are grouped by collection_name so an inheritance hierarchy whose subclasses share the base's collection (Mongoid STI, discriminated by the auto-added _type field) collapses into a single config that aggregates every class's fields and associations. See expand_with_descendants.

existing_by_name maps a collection name to its config already on disk, so the build can honor an explicit ignore: true (collection- or belongs_to-level) without re-introspecting it — and thus without aborting on a construct the user has deliberately ignored. Empty (the default) when called directly without an output dir, in which case nothing is honored.



117
118
119
120
121
# File 'lib/exwiw/mongoid_schema_generator.rb', line 117

def build_collections(existing_by_name = {})
  models_by_collection_name.map do |collection_name, group|
    build_collection_for(collection_name, group, existing_by_name[collection_name])
  end
end

#generate!Object



98
99
100
101
102
# File 'lib/exwiw/mongoid_schema_generator.rb', line 98

def generate!
  collections = build_collections(existing_configs_by_name(@output_dir))
  write_files(@output_dir, collections)
  collections
end

#tidy!Object

Reconcile the config files already on disk against the models, deleting the config of a collection no model stores into any more. This is the counterpart of generate!, which adds and updates config files but can never delete one: a collection whose model was removed leaves a config that would otherwise be dumped forever.

Fields are deliberately NOT touched: generate!'s #merge already drives the field list from the model, so a field the model lost is dropped there.

Unlike SchemaGenerator#tidy!, the source of truth is the model set, not a live connection. MongoDB has no schema to read: a collection exists only once a document is written to it, so "still in the database" is not a question that can be answered before a dump, and introspection here is class-level (see this class's preamble) and needs no connection at all. The name set therefore comes from exactly the grouping generate! writes files from (models_by_collection_name, descendants expanded and embedded models included), so the two can never disagree about which files are live.

A config declaring embedded_in is never deleted, whatever its name — see #hand_written_embedded_config?.

Returns a SchemaGenerator::TidyResult — reused rather than duplicated, since a removal report is the same shape whatever the ORM — describing the removals so callers (e.g. the rake task) can report them. Its removed_columns stays empty, for the reason above.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/exwiw/mongoid_schema_generator.rb', line 148

def tidy!
  result = SchemaGenerator::TidyResult.new
  return result unless @output_dir && File.directory?(@output_dir)

  live_names = models_by_collection_name.keys

  Dir[File.join(@output_dir, "*.json")].sort.each do |path|
    config = read_raw_config(path)

    # A file that does not parse says nothing about what it describes: its
    # basename would stand in for the collection name, and for a hand-written
    # embedded config that basename matches no collection by construction —
    # so the guard below would be bypassed for exactly the files it exists to
    # protect, and a stray merge-conflict marker would be enough to delete a
    # collection's masking rules. Broken JSON already fails loudly wherever
    # the config is loaded, so leave it in place and say so.
    if config.nil?
      warn("exwiw: skipping '#{path}' while tidying: it is not valid JSON, so what it describes cannot be determined.")
      next
    end

    name = declared_name(config, path)
    next if live_names.include?(name)
    next if hand_written_embedded_config?(config)

    File.delete(path)
    result.add_removed_table(name)
  end

  result
end

#write_files(dir, collections) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/exwiw/mongoid_schema_generator.rb', line 248

def write_files(dir, collections)
  FileUtils.mkdir_p(dir)

  collections.each do |collection|
    path = File.join(dir, "#{collection.name}.json")
    config_to_write =
      if File.exist?(path)
        MongodbCollectionConfig.from(JSON.parse(File.read(path))).merge(collection)
      else
        collection
      end
    File.write(path, JSON.pretty_generate(config_to_write.to_hash) + "\n")
  end
end