data_works 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|