data_works 0.1.1

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