dumped_railers 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e71ec228651de3ad8d735b37d3d091b1ac453df4e021a0545476a456eff5f7cb
4
+ data.tar.gz: da03643cbd834f41f59878ea1d7bf1411f2b739fc26a87e660e7b1f8d30c9fcd
5
+ SHA512:
6
+ metadata.gz: f53cd73227a90f14269c0dad160e3105bb3307c28274f45b7e2a96b44a22c7e9765cca115e23cc3a62dd0678afd17b0e4135f2d9c5035eb03a93a9e0e34b8fbb
7
+ data.tar.gz: f0d1cfad86c548302527028ccfa20d5ef10a16d24206ece6bdd8318f14768fa23aa7df06f68115ec27869fda3fa1297f3855e407d6b8cb67d3b4fd523bcb439a
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ **/*/tmp/
9
+ .rubocop-*
10
+
11
+ Gemfile.lock
12
+ *.sqlite3
13
+ *.log
14
+
15
+ # rspec failure tracking
16
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,12 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.2
6
+ - 2.6.6
7
+ - 2.5.8
8
+ gemfile:
9
+ - gemfiles/Gemfile.rails_6.1.0
10
+ - gemfiles/Gemfile.rails_6.0.3.4
11
+ - gemfiles/Gemfile.rails_5.2.4
12
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,10 @@
1
+ # Change Log
2
+
3
+ ## [0.1.0]
4
+ ### Added
5
+ - Implement method that dumps specified model data in fixture (YAML) format
6
+ - Implement import method that transfer fixture into database
7
+ - Add test helpers to make it easy to implement database related tests
8
+ - Add config options to be able to ignore specific columns
9
+ - Accept preprocessors to make filtering behavior pluggable and customizable
10
+ - Add Readme
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dumped_railers.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Koji Onishi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,165 @@
1
+ # DumpedRailers <img src='https://user-images.githubusercontent.com/23026542/101830310-aaf10000-3b77-11eb-9e0a-d14e45b27760.png' width=40>
2
+ [![Build Status](https://travis-ci.com/fursich/dumped_railers.svg?branch=main)](https://travis-ci.com/fursich/dumped_railers) [![Gem Version](https://badge.fury.io/rb/dumped_railers.svg)](https://badge.fury.io/rb/dumped_railers)
3
+
4
+ Helping you take a snapshot of ActiveRecord models in Rails-compatible fixture format, and re-importing them wherever necessary without destroying current data you have.
5
+
6
+ With Rails, you can import any fixture data using `rails db:fixtures:load` - however, this let's you remove all the existing data in your database before importing the fixtures. This is good for clean seeding, typically when running your tests, but there are other senarios where you want to merely ADD data, without damaging the current data you are woking with.
7
+
8
+ This is a bit trickey puzzle, though. To add the imported records, you cannot dump and re-import the primary key, as they are already taken by the original records. But usually your records involve reference to associated records, where the associations are guaranteed by the very primary keys.
9
+ In other words, the fixures have to be stored and re-imported, so as to maintain their original inter-dependencies, but their primary keys have to be re-assigned (headache)
10
+
11
+ DumpedRailers can add (not replace) the fixture without removing the current records, while restoring **all associations** among the original records.
12
+ Additionally, it can ignore, mask, or tweak any attributes when dumping the records into fixture files, which is convenient to export sensitive data (typically in your production environment) into, for instance, your staging environment.
13
+
14
+ This feature can particularily help you in the following senarios:
15
+ - you want to copy a group of records without breaking the other data sets you are working on
16
+ - you want to transfer some production data into dev environment, to reproduce some errors you encontered.
17
+ - you work for a multi-tenancy service, where you want to duplicate interdependent set of records from tenant A to tenant B.
18
+ - you simply want to populate interdependent record sets, which is not easy to re-build with FactoryBot
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'dumped_railers'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle install
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install dumped_railers
35
+
36
+ ## Usage
37
+
38
+ ### Getting Started
39
+
40
+ * if you want to dump (let's say) User, Item, and Tag models, just run the following.
41
+
42
+ ```ruby
43
+ DumpedRailers.dump!(User, Item, Tag, base_dir: 'tmp/fixtures/')
44
+ ```
45
+ this will generate three fixture files under tmp/fixtures/ folder.
46
+
47
+ * if you want to import the records you just saved, run:
48
+
49
+ ```ruby
50
+ DumpedRailers.import!('tmp/fixtures')
51
+ ```
52
+
53
+ * you can also specify individual model(s) for selective import.
54
+
55
+ ```ruby
56
+ DumpedRailers.import!('tmp/fixtures/users.yml', 'tmp/fixtures/items.yml')
57
+ ```
58
+
59
+ NOTE: you at least have to provide all the dependent records, so that DumpedRailers can resolve dependencies among the fixtures provided.
60
+
61
+ ### Ignored Columns
62
+
63
+ * By default, DumpedRailers ignore three columns - `id`, `created_at`, `updated_at`. You can always update/change this settings as follows.
64
+
65
+ ```ruby
66
+ DumpedRailers.configure do |config|
67
+ config.ignorable_columns += [:published_on] # published_on will be ignored on top of default settings.
68
+ end
69
+ ```
70
+
71
+ * of course you can totally replace the settings with your own.
72
+ ```ruby
73
+ DumpedRailers.configure do |config|
74
+ config.ignorable_columns = %i[uuid created_on updated_on] # uuid and created_on will be ignored instead of id, created_at, updated_at
75
+ end
76
+ ```
77
+
78
+ ### Masking, filtering
79
+
80
+ * you can pass `preprocessors` to DumpedRailers before it starts dump. All the attributes are filtered through preprocessors in order of registration.
81
+
82
+ ```ruby
83
+ DumpedRailers.dump!(User, Item, base_dir: 'tmp/', preprocessors: [MaskingPreprocessor.new])
84
+ ```
85
+
86
+ * "Preprocessors" can be lambda, or module, or any objects that can repond to #call(atrs, model).
87
+
88
+
89
+ ```ruby
90
+ class MaskingPreprocessor
91
+ def call(attrs, model)
92
+ attrs.map { |col, value|
93
+ col.match?(/password/) ? [col, '<MASKED>'] : [col, value]
94
+ }.to_h
95
+ end
96
+ end
97
+ ```
98
+
99
+ * a lambda object can be accepted as well
100
+
101
+ ```ruby
102
+ masking_preprocessor = -> (attrs, model) { attrs.transform_values(&:upcase) }
103
+ ```
104
+
105
+ NOTE: The proprocessors must return attributes in the same format `{ attributes_name: value }` so that preprocessors and dump handlers can preprocessors in nested manner.
106
+
107
+ ### pseudo multi-tenancy (such as ActsAsTenant)
108
+
109
+ * Such library builds multi-tenancy environment on one single database, using default_scope to switch over database access rights between tenants. You can incorporate data from Tenant A to Tenant B as follows. let's say we use [ActsAsTenant](https://github.com/ErwinM/acts_as_tenant)
110
+
111
+ ```ruby
112
+ # make sure to delete old fixtures in the folder
113
+ File.delete(Dir.glob('tmp/fixtures/{**,*}/*.yml'))
114
+
115
+ # let DumpedRailers ignore tenant column, as it will be overwritten by ActsAsTenant
116
+ DumpedRailers.configure do |config|
117
+ config.ignorable_columns += [:account_id]
118
+ end
119
+
120
+ # dump from tenant_a
121
+ ActsAsTenant.with_tenant(tenant_a) do
122
+ DumpedRailers.dump!(Item, Tag, base_dir: 'tmp/fixtures/')
123
+ end
124
+
125
+ # import into tenant_b
126
+ ActsAsTenant.with_tenant(tenant_b) do
127
+ DumpedRailers.import!('tmp/fixtures')
128
+ end
129
+ ```
130
+
131
+ ## Troubleshooting
132
+
133
+ When DumpedRailers fail to resolve dependencies, please check the following.
134
+
135
+ * DumpedRailers uses ActiveRecord reflection methods to sort out model dependencies. You might have to check wheather your relation is defined on the model (especially, `belongs_to` cannot be omitted).
136
+
137
+ * Dependencies cannot be resolved when cyclic dependencies are detected. For instance, the case like below cannot be resolved.
138
+
139
+ ```ruby
140
+ class Chicken < ActiveRecord::Base
141
+ belongs_to :egg, optional: true
142
+ end
143
+
144
+ class Egg < ActiveRecord::Base
145
+ belongs_to :chicken, optional: true
146
+ end
147
+ ```
148
+
149
+ * When exception raised, checking your log might give you a good hint (desperately staring at the backtrace won't give much information)
150
+ consider displaying `tail -f logs/development.log` while executing your script.
151
+
152
+ ## Development
153
+
154
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
155
+
156
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
157
+
158
+ ## Contributing
159
+
160
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[fursich]/dumped_railers.
161
+
162
+
163
+ ## License
164
+
165
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dumped_railers"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,37 @@
1
+ require_relative 'lib/dumped_railers/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'dumped_railers'
5
+ spec.version = DumpedRailers::VERSION
6
+ spec.authors = ['Koji Onishi']
7
+ spec.email = ['fursich0@gmail.com']
8
+
9
+ spec.summary = %q{A flexible fixture importer/exporter, that can transport ActiveRecord data in fixture format}
10
+ spec.description = %q{DumpedRailers helps you take a snapshot of ActiveRecord models in Rails-compatible fixture format, and re-import them wherever necessary without destroying current data you have.}
11
+ spec.homepage = 'https://github.com/fursich/dumped_railers'
12
+ spec.license = 'MIT'
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://github.com/fursich/dumped_railers'
17
+ spec.metadata['changelog_uri'] = 'https://github.com/fursich/dumped_railers/CHANGELOG.md'
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_development_dependency 'bundler', '~> 2.0'
29
+ spec.add_development_dependency 'rake', '~> 12.3.3'
30
+ spec.add_development_dependency 'rspec', '~> 3.0'
31
+ spec.add_development_dependency 'sqlite3'
32
+ spec.add_development_dependency 'activerecord', '~> 5.2'
33
+ spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8'
34
+ spec.add_development_dependency 'pry'
35
+ spec.add_development_dependency 'pry-byebug'
36
+ spec.add_development_dependency 'pry-doc'
37
+ end
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dumped_railers.gemspec
4
+ gem 'activerecord', '5.2.4'
5
+
6
+ gemspec path: '../'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dumped_railers.gemspec
4
+ gem 'activerecord', '6.1.0'
5
+
6
+ gemspec path: '../'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dumped_railers.gemspec
4
+ gem 'activerecord', '6.0.3.4'
5
+
6
+ gemspec path: '../'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dumped_railers/version'
4
+ require 'dumped_railers/file_helper.rb'
5
+ require 'dumped_railers/dump'
6
+ require 'dumped_railers/import'
7
+
8
+ module DumpedRailers
9
+ class << self
10
+
11
+ def dump!(*models, base_dir: './', preprocessors: nil)
12
+ preprocessors = [Preprocessor::StripIgnorables.new, *preprocessors].compact.uniq
13
+
14
+ fixture_handler = Dump.new(*models, preprocessors: preprocessors)
15
+ fixture_handler.build_fixtures!
16
+ fixture_handler.persist_all!(base_dir)
17
+ end
18
+
19
+ def import!(*paths)
20
+ # make sure class-baseed caches starts with clean state
21
+ DumpedRailers::RecordBuilder::FixtureRow::RecordStore.clear!
22
+ DumpedRailers::RecordBuilder::DependencyTracker.clear!
23
+
24
+ fixture_handler = Import.new(*paths)
25
+ fixture_handler.import_all!
26
+ end
27
+
28
+ class Configuration < ::OpenStruct; end
29
+
30
+ def config
31
+ @_config ||= Configuration.new
32
+ end
33
+
34
+ def configure
35
+ yield config
36
+ end
37
+
38
+ # FIXME: make it minimum
39
+ IGNORABLE_COLUMNS = %w[id created_at updated_at]
40
+ def configure_defaults!
41
+ configure do |config|
42
+ config.ignorable_columns = IGNORABLE_COLUMNS
43
+ end
44
+ end
45
+ end
46
+
47
+ configure_defaults!
48
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixture_builder/model'
4
+ require_relative 'preprocessor/strip_ignorables'
5
+
6
+ module DumpedRailers
7
+ class Dump
8
+ def initialize(*models, preprocessors: [])
9
+ @fixture_tables = models.map { |model|
10
+ FixtureBuilder::Model.new(model, preprocessors: preprocessors)
11
+ }
12
+ end
13
+
14
+ def build_fixtures!
15
+ @fixtures = @fixture_tables.map(&:build!).to_h
16
+ end
17
+
18
+ def persist_all!(base_dir)
19
+ FileUtils.mkdir_p(base_dir)
20
+ FileHelper.write(*@fixtures, base_dir: base_dir)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module DumpedRailers
6
+ module FileHelper
7
+ class << self
8
+ def read_fixtures(*paths)
9
+ yaml_files = paths.flat_map { |path|
10
+ if File.file?(path)
11
+ path
12
+ else
13
+ [*Dir["#{path}/{**,*}/*.yml"], "#{path}.yml"].select { |f|
14
+ ::File.file?(f)
15
+ }
16
+ end
17
+ }.uniq.compact
18
+
19
+ yaml_files.map { |file|
20
+ raw_data = ::File.read(file)
21
+ YAML.load(raw_data)
22
+ }
23
+ end
24
+
25
+ def write(*fixtures, base_dir:)
26
+ fixtures.each do |table_name, fixture|
27
+ pathname =
28
+ if defined?(Rails)
29
+ Rails.root.join("#{base_dir}/#{table_name}.yml")
30
+ else
31
+ Pathname.new("#{base_dir}/#{table_name}.yml")
32
+ end
33
+
34
+ pathname.open('w') do |f|
35
+ f.write fixture.to_yaml
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'record'
4
+
5
+ module DumpedRailers
6
+ module FixtureBuilder
7
+ class Model
8
+ def initialize(model, preprocessors:)
9
+ @model = model
10
+ @fixture_records = model.order(:id).map { |record|
11
+ Record.new(record, model, preprocessors: preprocessors)
12
+ }
13
+ end
14
+
15
+ def build!
16
+ fixture_body = @fixture_records.map(&:build!).to_h
17
+ fixture = fixture_body.reverse_merge build_fixture_header_for(@model)
18
+
19
+ [@model.table_name, fixture]
20
+ end
21
+
22
+ private
23
+
24
+ def build_fixture_header_for(model)
25
+ { '_fixture' =>
26
+ {
27
+ 'model_class' => model.name,
28
+ 'fixture_generated_by' => 'DumpedRailers',
29
+ }
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DumpedRailers
4
+ module FixtureBuilder
5
+ class Record
6
+ def initialize(record, model, preprocessors:)
7
+ @record = record
8
+ @model = model
9
+ @preprocessors = preprocessors
10
+ end
11
+
12
+ def build!
13
+ id = @record.id
14
+ attributes =
15
+ @preprocessors.inject(@record.attributes) { |attrs, preprocessor|
16
+ preprocessor.call(attrs, @model)
17
+ }
18
+
19
+ # convert "belong_to association" foreign keys into record-unique labels
20
+ @model.reflect_on_all_associations.select(&:belongs_to?).each do |rel|
21
+ # skip ignorables
22
+ next unless attributes.has_key? rel.foreign_key.to_s
23
+
24
+ if rel.polymorphic?
25
+ model_name = attributes[rel.foreign_type.to_s]
26
+
27
+ attributes[rel.name.to_s] = record_label_for(
28
+ model_name,
29
+ attributes.delete(rel.foreign_key.to_s),
30
+ attributes.delete(rel.foreign_type.to_s)
31
+ )
32
+ else
33
+ attributes[rel.name.to_s] = record_label_for(
34
+ rel.name,
35
+ attributes.delete(rel.foreign_key.to_s)
36
+ )
37
+ end
38
+ end
39
+
40
+ [record_label_for(@model.name, id), attributes]
41
+ end
42
+
43
+ private
44
+
45
+ def record_label_for(model_name, id, type=nil)
46
+ return nil unless id
47
+
48
+ identifier = "#{model_name.to_s.underscore}_#{id}"
49
+ type_specifier = "(#{type})" if type
50
+
51
+ "__#{identifier}#{type_specifier}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'record_builder/fixture_set'
4
+
5
+ module DumpedRailers
6
+ class Import
7
+ attr_reader :fixture_set
8
+
9
+ def initialize(*paths)
10
+ @raw_fixtures = FileHelper.read_fixtures(*paths)
11
+ @fixture_set = RecordBuilder::FixtureSet.new(@raw_fixtures)
12
+ end
13
+
14
+ def import_all!
15
+ fixture_set.sort_by_table_dependencies!
16
+ @record_sets = fixture_set.build_record_sets!
17
+
18
+ ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
19
+ # models have to be persisted one-by-one so that dependent models are able to
20
+ # resolve "belongs_to" (parent) association
21
+ @record_sets.each do |_model, records|
22
+ # FIXME: faster implementation wanted, parhaps with activerocord-import
23
+ # (objects needs to be reloaded somehow when using buik insert)
24
+ records.each(&:save!)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DumpedRailers
4
+ module Preprocessor
5
+ class StripIgnorables
6
+ def call(attributes, _model)
7
+ attributes.reject { |column_name, _v|
8
+ DumpedRailers.config.ignorable_columns.map(&:to_s).include?(column_name)
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DumpedRailers
4
+ module RecordBuilder
5
+ class DependencyTracker
6
+ class << self
7
+ def for(model)
8
+ trackers[model] ||= new
9
+ end
10
+
11
+ def clear!
12
+ @trackers = {}
13
+ end
14
+
15
+ private
16
+
17
+ def trackers
18
+ @trackers ||= {}
19
+ end
20
+ end
21
+
22
+ def on(record)
23
+ dependencies[record] ||= RecordDependency.new
24
+ end
25
+
26
+ def list_all_record_labels_with(attr)
27
+ list_all_dependencies_with(attr)
28
+ .map { |dependent| dependent.record_label }
29
+ .compact
30
+ end
31
+
32
+ def list_all_model_names_with(attr)
33
+ list_all_dependencies_with(attr)
34
+ .map { |dependent| dependent.model_name }
35
+ .compact
36
+ end
37
+
38
+ private
39
+
40
+ def dependencies
41
+ @dependencies ||= {}
42
+ end
43
+
44
+ def list_all_dependencies_with(attr)
45
+ dependencies
46
+ .values
47
+ .map { |record_dependency|
48
+ record_dependency.with(attr)
49
+ }
50
+ end
51
+
52
+ class RecordDependency
53
+ def with(attr)
54
+ record_dependency[attr.to_sym] ||= DependentObject.new
55
+ end
56
+
57
+ def each_dependent_record_label(&block)
58
+ return enum_for(:each_dependent_record_label) unless block_given?
59
+
60
+ record_dependency.each { |attr, dependent_object|
61
+ block.call(attr, dependent_object.record_label)
62
+ }
63
+ end
64
+
65
+ def dependent_record_labels
66
+ record_dependency.values.map(&:record_label).compact
67
+ end
68
+
69
+ private
70
+
71
+ def record_dependency
72
+ @record_dependency ||= {}
73
+ end
74
+
75
+ class DependentObject
76
+ attr_accessor :record_label, :model_name
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DumpedRailers
4
+ module RecordBuilder
5
+ class FixtureRow
6
+ attr_reader :label, :attrs
7
+
8
+ def initialize(label, attrs)
9
+ @label = label
10
+ @attrs = attrs
11
+ end
12
+
13
+ def analyze_dependencies!(dependency_tracker)
14
+ raise RuntimeError, 'Can\'t execute the dependency analysis twice. This has been done already' if @dependency
15
+
16
+ @dependency = dependency_tracker
17
+
18
+ attrs.each do |attr, value|
19
+ ref, model_name = parse_reference_from(value)
20
+ next unless ref
21
+
22
+ attrs[attr] = ref.to_sym
23
+ @dependency.with(attr).record_label = ref.to_sym
24
+ @dependency.with(attr).model_name = model_name&.to_sym
25
+ end
26
+ end
27
+
28
+ def instantiate_as!(model)
29
+ raise RuntimeError, 'Could not find the dependency tracker. Run #analyze_dependencies to instaitiate the records' unless @dependency
30
+
31
+ @model = model
32
+ resolve_reference!
33
+ object = model.new(attrs)
34
+ RecordStore.register(label, object: object)
35
+
36
+ object
37
+ end
38
+
39
+ private
40
+
41
+ def parse_reference_from(val)
42
+ # NOTE: make sure its object is a string (can be a json object)
43
+ return unless val.is_a? String
44
+ return unless val.start_with? '__'
45
+
46
+ # format convention
47
+ # for non-polymorphic association: __[identifier]
48
+ # for polymorphic association: __[identifier]([model_name])
49
+ ref, _, model_name = val.scan(/\A(__[^(\s]+)(\(([^)]+)\))?\z/).first
50
+
51
+ [ref, model_name]
52
+ end
53
+
54
+ def resolve_reference!
55
+ raise RuntimeError, <<~"ERROR_MESSAGE" unless resolvable?
56
+ cannot resolve dependencies. (some fixtures might be missing)
57
+ model: #{@model}
58
+ record: #{label}
59
+ ERROR_MESSAGE
60
+
61
+ @dependency.each_dependent_record_label { |attr, record_label|
62
+ attrs[attr] = RecordStore.retrieve!(record_label)
63
+ }
64
+ end
65
+
66
+ def resolvable?
67
+ @dependency.dependent_record_labels.all? { |label|
68
+ RecordStore.registered?(label)
69
+ }
70
+ end
71
+
72
+ class RecordStore
73
+ class << self
74
+ def register(label, object:)
75
+ set_object(label, object)
76
+ end
77
+
78
+ def registered?(label)
79
+ !object_for(label).nil?
80
+ end
81
+
82
+ def retrieve!(label)
83
+ raise RuntimeError, "couldn't resolve dependent record: #{label}" unless registered?(label)
84
+
85
+ object_for(label)
86
+ end
87
+
88
+ def clear!
89
+ @repository = {}
90
+ end
91
+
92
+ private
93
+
94
+ def repository
95
+ @repository ||= {}
96
+ end
97
+
98
+ def object_for(label)
99
+ repository[label.to_sym]
100
+ end
101
+
102
+ def set_object(label, object)
103
+ repository[label.to_sym] = object
104
+ end
105
+ end
106
+
107
+ def initialize
108
+ # use this class as (some sort of) Singleton
109
+ raise NotImplementedError
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixture_table'
4
+ require 'tsort'
5
+
6
+ module DumpedRailers
7
+ module RecordBuilder
8
+ class FixtureSet
9
+ include TSort
10
+ attr_reader :fixture_tables, :record_sets
11
+
12
+ def initialize(raw_fixtures)
13
+ @fixture_tables = raw_fixtures.map { |raw_records| build_fixture_table(raw_records) }
14
+ end
15
+
16
+ def sort_by_table_dependencies!
17
+ @fixture_tables.each(&:analyze_metadata_dependencies!)
18
+ # dependency are sorted in topological order using Active Record reflection
19
+ @fixture_tables = tsort
20
+
21
+ self
22
+ end
23
+
24
+ def build_record_sets!
25
+ @record_sets = @fixture_tables.map { |table|
26
+ [table.model, table.build_records!]
27
+ }.to_h
28
+ end
29
+
30
+ private
31
+
32
+ def build_fixture_table(raw_records)
33
+ FixtureTable.new(raw_records)
34
+ end
35
+
36
+ def tsort_each_node(&block)
37
+ @fixture_tables.each { |table| block.call(table) }
38
+ end
39
+
40
+ def tsort_each_child(node, &block)
41
+ dependent_nodes = @fixture_tables.select { |table| node.dependencies.include? table.model_name }
42
+ dependent_nodes.each &block
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixture_row'
4
+ require_relative 'dependency_tracker'
5
+
6
+ module DumpedRailers
7
+ module RecordBuilder
8
+ class FixtureTable
9
+ attr_reader :model, :model_name, :rows, :objects, :dependencies
10
+
11
+ def initialize(raw_records)
12
+ config = raw_records.delete('_fixture')
13
+
14
+ @model = identify_model!(config)
15
+ @model_name = model.name.to_sym
16
+ @dependency_tracker = DependencyTracker.for(model)
17
+
18
+ @rows = raw_records.map { |label, attrs|
19
+ build_fixture_row(label, attrs)
20
+ }
21
+ end
22
+
23
+ def analyze_metadata_dependencies!
24
+ raise RuntimeError, "Dependency Analysis has already been done with the fixture for #{model_name}" if @dependencies
25
+
26
+ rows.map { |row| row.analyze_dependencies!(@dependency_tracker.on(row)) }
27
+
28
+ @dependencies = model.reflect_on_all_associations.select(&:belongs_to?).flat_map { |rel|
29
+ if rel.polymorphic?
30
+ @dependency_tracker.list_all_model_names_with(rel.name)
31
+ else
32
+ rel.class_name.to_sym
33
+ end
34
+ }.uniq
35
+ end
36
+
37
+ def build_records!
38
+ raise RuntimeError, "The records in this fixture for #{model_name} have been built already" if @instantiated
39
+
40
+ @objects = rows.map { |row| row.instantiate_as!(model) }
41
+ @instantiated = true
42
+
43
+ objects
44
+ end
45
+
46
+ private
47
+
48
+ def build_fixture_row(label, attrs)
49
+ FixtureRow.new(
50
+ label.to_sym,
51
+ attrs.symbolize_keys,
52
+ )
53
+ end
54
+
55
+ def identify_model!(config)
56
+ model_name = config&.dig('model_class')
57
+ raise RuntimeError, <<~"ERROR_MESSAGE" unless model_name
58
+ couldn't find `_fixture: model_class` label in the fixture.
59
+ (possibly not an auto-generated one?)
60
+ ERROR_MESSAGE
61
+
62
+ model = model_name.safe_constantize
63
+ return model if model && model < ActiveRecord::Base
64
+
65
+ raise RuntimeError, <<~"ERROR_MESSAGE"
66
+ couldn't find a model named #{model_name} specified with `_fixture: model_class` label in the fixture.
67
+ you might want to check whether:
68
+ - this task runs in the same application that the fixtures were generated in
69
+ - relevant tables have not been altered or dropped since the fixtures were generated
70
+ ERROR_MESSAGE
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module DumpedRailers
2
+ VERSION = "0.1.1"
3
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dumped_railers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Koji Onishi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-12-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: database_cleaner-active_record
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.8'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-doc
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: DumpedRailers helps you take a snapshot of ActiveRecord models in Rails-compatible
140
+ fixture format, and re-import them wherever necessary without destroying current
141
+ data you have.
142
+ email:
143
+ - fursich0@gmail.com
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - ".gitignore"
149
+ - ".rspec"
150
+ - ".travis.yml"
151
+ - CHANGELOG.md
152
+ - Gemfile
153
+ - LICENSE.txt
154
+ - README.md
155
+ - Rakefile
156
+ - bin/console
157
+ - bin/setup
158
+ - dumped_railers.gemspec
159
+ - gemfiles/Gemfile.rails_5.2.4
160
+ - gemfiles/Gemfile.rails_6.0.3.4
161
+ - gemfiles/Gemfile.rails_6.1.0
162
+ - lib/dumped_railers.rb
163
+ - lib/dumped_railers/dump.rb
164
+ - lib/dumped_railers/file_helper.rb
165
+ - lib/dumped_railers/fixture_builder/model.rb
166
+ - lib/dumped_railers/fixture_builder/record.rb
167
+ - lib/dumped_railers/import.rb
168
+ - lib/dumped_railers/preprocessor/strip_ignorables.rb
169
+ - lib/dumped_railers/record_builder/dependency_tracker.rb
170
+ - lib/dumped_railers/record_builder/fixture_row.rb
171
+ - lib/dumped_railers/record_builder/fixture_set.rb
172
+ - lib/dumped_railers/record_builder/fixture_table.rb
173
+ - lib/dumped_railers/version.rb
174
+ homepage: https://github.com/fursich/dumped_railers
175
+ licenses:
176
+ - MIT
177
+ metadata:
178
+ homepage_uri: https://github.com/fursich/dumped_railers
179
+ source_code_uri: https://github.com/fursich/dumped_railers
180
+ changelog_uri: https://github.com/fursich/dumped_railers/CHANGELOG.md
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: 2.3.0
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubygems_version: 3.0.3
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: A flexible fixture importer/exporter, that can transport ActiveRecord data
200
+ in fixture format
201
+ test_files: []