Class: Exwiw::MongoidSchemaGenerator
- Inherits:
-
Object
- Object
- Exwiw::MongoidSchemaGenerator
- 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
DefaultMaskinterpolates 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 symbolsDefaultMask.forunderstands, 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::Booleanhere would raise. A name comparison also sidestepsDateTime < 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_decisionflag 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
-
.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.
Instance Method Summary collapse
-
#build_collections(existing_by_name = {}) ⇒ Object
Returns an array of
MongodbCollectionConfig, one per collection (top-level collections and embedded subdocument configs alike). - #generate! ⇒ Object
-
#initialize(models:, output_dir:, skip_unsupported: false, safe_new_columns: true) ⇒ MongoidSchemaGenerator
constructor
safe_new_columns(the default) emits every field masked — as far as its Mongoid type allows, see MASKABLE_FIELD_TYPES and DefaultMask — and flaggedneeds_mask_decision: true. -
#tidy! ⇒ Object
Reconcile the config files already on disk against the models, deleting the config of a collection no model stores into any more.
- #write_files(dir, collections) ⇒ Object
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 (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 |