data_works 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +529 -0
- data/Rakefile +6 -0
- data/_config.yml +1 -0
- data/bin/_guard-core +17 -0
- data/bin/guard +17 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/data_works.gemspec +35 -0
- data/lib/data_works.rb +15 -0
- data/lib/data_works/base.rb +26 -0
- data/lib/data_works/config.rb +19 -0
- data/lib/data_works/exceptions.rb +6 -0
- data/lib/data_works/grafter.rb +51 -0
- data/lib/data_works/necessary_parent.rb +27 -0
- data/lib/data_works/parent_creator.rb +67 -0
- data/lib/data_works/railtie.rb +17 -0
- data/lib/data_works/relationships.rb +29 -0
- data/lib/data_works/stale_relationship_checker.rb +87 -0
- data/lib/data_works/version.rb +4 -0
- data/lib/data_works/visualization.rb +127 -0
- data/lib/data_works/works.rb +111 -0
- data/lib/tasks/bless.rake +6 -0
- data/spec/adding_records_spec.rb +118 -0
- data/spec/factories/factories.rb +78 -0
- data/spec/helper/data_works_spec_helper.rb +46 -0
- data/spec/helper/test_models.rb +106 -0
- data/spec/helper/test_tables.rb +120 -0
- data/spec/lib/data_faker.rb +37 -0
- data/spec/relationships_spec.rb +108 -0
- data/spec/restricted_parentage_spec.rb +35 -0
- data/spec/singluar_ending_in_es_spec.rb +31 -0
- data/spec/spec_helper.rb +33 -0
- metadata +292 -0
data/Rakefile
ADDED
data/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
theme: jekyll-theme-minimal
|
data/bin/_guard-core
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application '_guard-core' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("guard", "_guard-core")
|
data/bin/guard
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'guard' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("guard", "guard")
|
data/bin/rake
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rake' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/data_works.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "data_works/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "data_works"
|
8
|
+
s.version = DataWorks::VERSION
|
9
|
+
s.authors = ["Wyatt Greene", "Anne Geiersbach", "Dennis Chan", "Luke Inglis"]
|
10
|
+
s.email = ["ld.inglis@gmail.com"]
|
11
|
+
s.summary = %q{Reducing the complexity of testing complex data models }
|
12
|
+
s.description = %q{DataWorks makes it easier to work with FactoryBot in the context of a complex data model.}
|
13
|
+
s.homepage = 'https://github.com/ludamillion/data_works'
|
14
|
+
s.licenses = ["MIT", "Copyright (c) 2018 District Management Group"]
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "activerecord", "~> 4.1"
|
21
|
+
s.add_dependency "activesupport", "~> 4.1"
|
22
|
+
s.add_dependency 'factory_girl', '>= 3.0'
|
23
|
+
s.add_dependency 'graphviz', '~> 0.1.0'
|
24
|
+
s.add_dependency 'launchy', '~> 2.4'
|
25
|
+
|
26
|
+
s.add_development_dependency "bundler", "~> 1.9"
|
27
|
+
s.add_development_dependency "rake", "~> 10.4"
|
28
|
+
s.add_development_dependency "database_cleaner", "~> 1.4.0"
|
29
|
+
s.add_development_dependency "rspec"
|
30
|
+
s.add_development_dependency "sqlite3"
|
31
|
+
s.add_development_dependency "pry"
|
32
|
+
s.add_development_dependency "guard"
|
33
|
+
s.add_development_dependency "guard-rspec"
|
34
|
+
s.add_development_dependency "active_hash", "~> 1.5.0"
|
35
|
+
end
|
data/lib/data_works.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'data_works/version'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
|
5
|
+
require 'data_works/exceptions'
|
6
|
+
require 'data_works/stale_relationship_checker'
|
7
|
+
require 'data_works/relationships'
|
8
|
+
require 'data_works/visualization'
|
9
|
+
require 'data_works/necessary_parent'
|
10
|
+
require 'data_works/parent_creator'
|
11
|
+
require 'data_works/grafter'
|
12
|
+
require 'data_works/works'
|
13
|
+
require 'data_works/base'
|
14
|
+
require 'data_works/config'
|
15
|
+
require "data_works/railtie" if defined?(Rails)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# This class wraps DataWorks::Works and only exposes methods that are
|
2
|
+
# meant to be public. The sole purpose of this class is information
|
3
|
+
# hiding.
|
4
|
+
module DataWorks
|
5
|
+
class Base
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@works = DataWorks::Works.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# we expose the public interface here
|
12
|
+
def method_missing(method_name, *args, &block)
|
13
|
+
method_name = method_name.to_s
|
14
|
+
if method_name =~ /\A(add_|the_)(\w+)\Z/ ||
|
15
|
+
method_name =~ /\A(\w+)(\d+)\Z/ ||
|
16
|
+
method_name =~ /\Aset_(current_default|restriction)\Z/ ||
|
17
|
+
method_name =~ /\Aclear_(current_default|restriction)_for\Z/ ||
|
18
|
+
method_name == 'visualize'
|
19
|
+
@works.send(method_name, *args, &block)
|
20
|
+
else
|
21
|
+
raise NoMethodError.new("#{method_name} method not found in data works")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DataWorks
|
2
|
+
|
3
|
+
def self.configure
|
4
|
+
yield(Config)
|
5
|
+
end
|
6
|
+
|
7
|
+
class Config
|
8
|
+
|
9
|
+
def self.necessary_parents=(hash)
|
10
|
+
Relationships.necessary_parents = hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.autocreated_children=(hash)
|
14
|
+
Relationships.autocreated_children = hash
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DataWorks
|
2
|
+
class Grafter
|
3
|
+
|
4
|
+
class ModelCreator
|
5
|
+
def initialize(works, model_name, model_attrs)
|
6
|
+
@works = works
|
7
|
+
@model_name = model_name.to_sym
|
8
|
+
@model_attrs = model_attrs
|
9
|
+
@parent_creator = ParentCreator.new(@works, @model_name, @model_attrs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_model_and_its_necessary_parents
|
13
|
+
created_parents = @parent_creator.create_necessary_parents(parents_we_already_have)
|
14
|
+
FactoryGirl.create(@model_name, @model_attrs.merge(created_parents))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# If we use DataWorks like this:
|
20
|
+
# data.add_student(:school => some_school)
|
21
|
+
# then we are passing in a necessary parent model (the school), so
|
22
|
+
# DataWorks does not have to autogenerate it, since we already have it.
|
23
|
+
def parents_we_already_have
|
24
|
+
provided_attribute_names = @model_attrs.keys
|
25
|
+
necessary_parent_names = Relationships.necessary_parents_for(@model_name).map(&:association_name)
|
26
|
+
provided_attribute_names & necessary_parent_names
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(works, model_name)
|
31
|
+
@works = works
|
32
|
+
@model_name = model_name.to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_many(number, model_attrs={})
|
36
|
+
new_models = []
|
37
|
+
number.times do
|
38
|
+
new_models << add_one(model_attrs)
|
39
|
+
end
|
40
|
+
new_models
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_one(model_attrs={})
|
44
|
+
model_creator = ModelCreator.new(@works, @model_name, model_attrs)
|
45
|
+
new_model = model_creator.create_model_and_its_necessary_parents
|
46
|
+
@works.was_added(@model_name, new_model)
|
47
|
+
new_model
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# The purpose of this class is to encapsulate the idea that when configuring
|
2
|
+
# DataWorks, necessary_parents can include symbols or hashes, like so:
|
3
|
+
#
|
4
|
+
# config.necessary_parents = {
|
5
|
+
# district: [ ],
|
6
|
+
# event: [:schedule, :school],
|
7
|
+
# scheduled_service: [{:schedulable => :event}, :student],
|
8
|
+
# school: [:district]
|
9
|
+
# student: [:school]
|
10
|
+
# }
|
11
|
+
#
|
12
|
+
module DataWorks
|
13
|
+
class NecessaryParent
|
14
|
+
|
15
|
+
attr_reader :association_name, :model_name
|
16
|
+
|
17
|
+
def initialize(entry)
|
18
|
+
if entry.is_a? Hash
|
19
|
+
@association_name = entry.keys.first
|
20
|
+
@model_name = entry.values.first
|
21
|
+
else
|
22
|
+
@association_name = @model_name = entry
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module DataWorks
|
2
|
+
class ParentCreator
|
3
|
+
|
4
|
+
def initialize(works, model_name, model_attrs)
|
5
|
+
@works = works
|
6
|
+
@model_name = model_name.to_sym
|
7
|
+
@parents = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# This returns a hash of the parent models created, so that they can be
|
11
|
+
# easily merged into a model attribute hash. For example, if a school
|
12
|
+
# parent model was created for a student model, then it would be returned
|
13
|
+
# like so:
|
14
|
+
# { :school => the_school_model }
|
15
|
+
def create_necessary_parents(parents_we_already_have)
|
16
|
+
for missing_necessary_parent in missing_necessary_parents(parents_we_already_have)
|
17
|
+
find_or_add(missing_necessary_parent)
|
18
|
+
end
|
19
|
+
@parents
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def missing_necessary_parents(parents_we_already_have)
|
25
|
+
Relationships.necessary_parents_for(@model_name).reject do |necessary_parent|
|
26
|
+
parents_we_already_have.include?(necessary_parent.association_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_or_add(necessary_parent)
|
31
|
+
parent_model = @works.find_or_add(necessary_parent.model_name)
|
32
|
+
destroy_zombies(parent_model, necessary_parent.association_name)
|
33
|
+
@parents[necessary_parent.association_name] = parent_model
|
34
|
+
end
|
35
|
+
|
36
|
+
# Consider the case where a model has a has_one relationship with its
|
37
|
+
# child. The model autocreates the child model using a callback like
|
38
|
+
# before_validation or after_initialize.
|
39
|
+
#
|
40
|
+
# This can confuse DataWorks because DataWorks creates models by starting
|
41
|
+
# at the child and creating all of the necessary ancestors. It's a
|
42
|
+
# "child creates parent" strategy. When a model class autocreates a
|
43
|
+
# child, it happens outside of DataWorks so DataWorks does not know about
|
44
|
+
# it and it goes against the flow since it has a "parent creates child"
|
45
|
+
# strategy.
|
46
|
+
#
|
47
|
+
# The upshot is that a bunch of unmanaged zombie objects will be lying
|
48
|
+
# around that could disrupt tests. So we specifically have to delete
|
49
|
+
# any model objects that a parent may have created outside of DataWorks
|
50
|
+
# before they cause trouble.
|
51
|
+
def destroy_zombies(parent_model, parent_association_name)
|
52
|
+
if zombies_possible?(parent_association_name)
|
53
|
+
destroy_zombie_child_of(parent_model)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def zombies_possible?(parent_association_name)
|
58
|
+
Relationships.autocreated_children_of(parent_association_name).include?(@model_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def destroy_zombie_child_of(parent_model)
|
62
|
+
child = parent_model.send(@model_name)
|
63
|
+
child.destroy if child.persisted? && !child.destroyed?
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'data_works'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module DataWorks
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
railtie_name :data_works
|
7
|
+
|
8
|
+
rake_tasks do
|
9
|
+
namespace :data_works do
|
10
|
+
desc "tell data_works that the model relationships are accurate"
|
11
|
+
task :bless => :environment do
|
12
|
+
DataWorks::StaleRelationshipChecker.create_snapshot!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module DataWorks
|
2
|
+
class Relationships
|
3
|
+
|
4
|
+
def self.autocreated_children=(hash)
|
5
|
+
@autocreated_children = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.autocreated_children_of(model_name)
|
9
|
+
@autocreated_children[model_name] || []
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.necessary_parents=(hash)
|
13
|
+
@necessary_parents = hash
|
14
|
+
StaleRelationshipChecker.check!
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.necessary_parents_for(model_name)
|
18
|
+
result = @necessary_parents[model_name]
|
19
|
+
if result.nil?
|
20
|
+
message = "The model '#{model_name}' is not registered. "
|
21
|
+
message << "It should be registered in the DataWorks.configure section "
|
22
|
+
message << "of your spec_helper.rb file."
|
23
|
+
raise DataWorksError.new(message)
|
24
|
+
end
|
25
|
+
result.map{|x| NecessaryParent.new(x)}
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module DataWorks
|
2
|
+
class StaleRelationshipChecker
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def check!
|
6
|
+
if snapshot_exists?
|
7
|
+
check_for_staleness!
|
8
|
+
else
|
9
|
+
create_snapshot!
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_snapshot!
|
14
|
+
File.open(filepath, 'w') do |f|
|
15
|
+
f.puts explanatory_comments
|
16
|
+
f.puts "DataWorks::MOST_RECENT_SNAPSHOT = ["
|
17
|
+
current_snapshot.each do |class_name, belongs_to_name|
|
18
|
+
f.puts " ['#{class_name}', #{belongs_to_name}],"
|
19
|
+
end
|
20
|
+
f.puts "]"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_for_staleness!
|
27
|
+
load_snapshot
|
28
|
+
saved = DataWorks::MOST_RECENT_SNAPSHOT
|
29
|
+
current = current_snapshot
|
30
|
+
if saved != current
|
31
|
+
differences = (saved - current) + (current - saved)
|
32
|
+
message = "DataWorks has detected changes to your data model. "
|
33
|
+
message << "DataWorks requires that the config.necessary_parents "
|
34
|
+
message << "in your spec_helper.rb file be an accurate description "
|
35
|
+
message << "of what parent objects need to be automatically "
|
36
|
+
message << "factoried. Please update this configuration to be "
|
37
|
+
message << "accurate, if necessary. Then run rake data_works:bless "
|
38
|
+
message << "to tell DataWorks that the configuration is now "
|
39
|
+
message << "up-to-date."
|
40
|
+
message << "\nHere's a hint as to where the data model changes "
|
41
|
+
message << "are #{differences.inspect}."
|
42
|
+
raise ModelRelationshipsOutOfDateError.new(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def snapshot_exists?
|
47
|
+
File.exists?(filepath)
|
48
|
+
end
|
49
|
+
|
50
|
+
def explanatory_comments
|
51
|
+
<<-COMMENTS.strip_heredoc
|
52
|
+
# THIS IS A GENERATED FILE. DO NOT EDIT.
|
53
|
+
#
|
54
|
+
# Generated on #{Time.now.inspect}.
|
55
|
+
#
|
56
|
+
# This file is used by DataWorks to check whether data model
|
57
|
+
# changes have occurred. When you change the data model, you
|
58
|
+
# need to update DataWorks' configuration. See the DataWorks
|
59
|
+
# README for details.
|
60
|
+
#
|
61
|
+
COMMENTS
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_snapshot
|
65
|
+
all_active_record_classes.map do |ar_class|
|
66
|
+
[ar_class.name, ar_class.reflect_on_all_associations(:belongs_to).map(&:name)]
|
67
|
+
end.sort_by(&:first)
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_snapshot
|
71
|
+
load filepath
|
72
|
+
end
|
73
|
+
|
74
|
+
def filepath
|
75
|
+
File.join(Rails.root, 'db', 'data_works_relationship_snapshot.rb')
|
76
|
+
end
|
77
|
+
|
78
|
+
def all_active_record_classes
|
79
|
+
@all_classes ||= begin
|
80
|
+
Rails.application.eager_load!
|
81
|
+
ActiveRecord::Base.send(:descendants)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|