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.
@@ -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 @@
1
+ theme: jekyll-theme-minimal
@@ -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")
@@ -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")
@@ -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")
@@ -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")
@@ -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
@@ -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,6 @@
1
+ module DataWorks
2
+
3
+ class DataWorksError < StandardError; end
4
+ class ModelRelationshipsOutOfDateError < DataWorksError; end
5
+
6
+ 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