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,4 @@
1
+ module DataWorks
2
+ VERSION = "0.1.1"
3
+ end
4
+
@@ -0,0 +1,127 @@
1
+ require 'graphviz'
2
+ require 'launchy'
3
+
4
+ module DataWorks
5
+ module Visualization
6
+
7
+ def visualize
8
+ @g = Graphviz::Graph.new
9
+ build_nodes
10
+ connect_nodes
11
+ filename = "factoried-data-diagram-#{Time.new.strftime("%Y%m%d%H%M%S")}#{rand(1000)}.png"
12
+ path = begin
13
+ File.join(Rails.root, "tmp", filename)
14
+ rescue NameError => error # if not in Rails environment i.e. dev testing
15
+ root = File.expand_path('../..', File.dirname(__FILE__))
16
+ File.join root, 'tmp', filename
17
+ end
18
+ Graphviz::output(@g, path: path)
19
+ Launchy.open(path)
20
+ end
21
+
22
+ private
23
+
24
+ def get_node_shape(model)
25
+ model.is_a?(ActiveRecord::Base) ? 'oval' :'diamond'
26
+ end
27
+
28
+ def build_nodes
29
+ @nodes = {}
30
+ @data.keys.each do |model_name|
31
+ @nodes[model_name] = @data[model_name].each_with_index.map do |model, i|
32
+ @g.add_node("#{model_name}#{i+1}", shape: get_node_shape(model))
33
+ end
34
+ end
35
+ end
36
+
37
+ def connect_nodes
38
+ model_names = @data.keys
39
+ for model_name1 in model_names
40
+ for model_name2 in model_names
41
+ connect_model_kinds(model_name1, model_name2)
42
+ end
43
+ end
44
+ connect_non_active_record_nodes
45
+ end
46
+
47
+ def connect_non_active_record_nodes
48
+ model_names = @data.keys.select do |k|
49
+ k.to_s.classify.constantize.new.is_a?(ActiveHash::Base)
50
+ end
51
+
52
+ model_names.each do |m|
53
+ children = @data.values.
54
+ flatten.
55
+ reject{|e| e.is_a?(m.to_s.classify.constantize)}.
56
+ select { |e| !!e.class.reflect_on_association(m) }
57
+
58
+ parents = @data[m]
59
+
60
+ children.each do |child|
61
+ parent = parents.detect { |p| p.id == child.send("#{m}_id") }
62
+ connect(parent, child)
63
+ end
64
+ end
65
+ end
66
+
67
+ # accepts symbols like :district, :district_schedule_context
68
+ def connect_model_kinds(model_type1, model_type2)
69
+ return if model_type1 == model_type2
70
+ models = @data[model_type1]
71
+ models.each do |parent|
72
+ find_and_connect_children_to_parent(parent, model_type2)
73
+ end
74
+ end
75
+
76
+ def find_and_connect_children_to_parent(parent, child_type)
77
+ assoc = child_association(parent, child_type)
78
+ return if assoc.nil?
79
+ parent.reload
80
+ children = [parent.send(assoc)].flatten
81
+ for child in children
82
+ connect(parent, child)
83
+ end
84
+ end
85
+
86
+ def child_association(parent, child_type)
87
+ assoc = child_associations_for(parent).detect do |name, obj|
88
+ name.to_s.singularize.to_sym == child_type.to_s.singularize.to_sym
89
+ end
90
+ assoc.try(:first)
91
+ end
92
+
93
+ def child_associations_for(parent)
94
+ return [] unless parent.class.respond_to?(:reflections)
95
+ parent.class.reflections.to_a.select do |name, obj|
96
+ (obj.macro == :has_many || obj.macro == :has_one) && !obj.options[:through]
97
+ end
98
+ end
99
+
100
+ def connect(parent, child)
101
+ child_model_name = model_name_of(child)
102
+ parent_model_name = model_name_of(parent)
103
+ if @data[parent_model_name]
104
+ i = @data[parent_model_name].find_index { |model| model.id == parent.id }
105
+ parent_node = @nodes[parent_model_name][i]
106
+ if parent_node
107
+ if @data[child_model_name]
108
+ i = @data[child_model_name].find_index { |model| model.id == child.id }
109
+ if i.nil? # this factory was not created via the DataWorks
110
+ child_node = @g.add_node("#{child_model_name} (unmanaged)", shape: get_node_shape(child))
111
+ else
112
+ child_node = @nodes[child_model_name][i]
113
+ end
114
+ if child_node
115
+ parent_node.connect(child_node)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def model_name_of(model)
123
+ model.class.name.underscore.to_sym
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,111 @@
1
+ module DataWorks
2
+ class Works
3
+
4
+ include Visualization
5
+
6
+ def initialize
7
+ # we keep a registry of all models that we create
8
+ @data = {}
9
+ # keep a registry of the 'current default' model of a given type
10
+ @current_default = {}
11
+ # keep a registry of the 'limiting scope' for parentage
12
+ @bounding_models = {}
13
+ end
14
+
15
+ def method_missing(method_name, *args, &block)
16
+ method_name = method_name.to_s
17
+ if method_name.starts_with? 'add_'
18
+ add_model(method_name, *args)
19
+ else
20
+ get_model(method_name, *args)
21
+ end
22
+ end
23
+
24
+ def find_or_add(model_name)
25
+ record = find(model_name, 1)
26
+ record ? record : send("add_#{model_name}")
27
+ end
28
+
29
+ def was_added(model_name, model)
30
+ model_name = model_name.to_sym
31
+ @data[model_name] ||= []
32
+ @data[model_name] << model
33
+ end
34
+
35
+ def set_current_default(for_model:, to:)
36
+ @current_default[for_model] = to
37
+ end
38
+
39
+ def clear_current_default_for(model)
40
+ @current_default.delete(model)
41
+ end
42
+
43
+ def set_restriction(for_model:, to:, &block)
44
+ if block_given?
45
+ @bounding_models[for_model] = to
46
+ block_return = block.call
47
+ clear_restriction_for(for_model)
48
+ block_return
49
+ elsif
50
+ @bounding_models[for_model] = to
51
+ to
52
+ end
53
+ end
54
+
55
+ def clear_restriction_for(model)
56
+ @bounding_models.delete(model)
57
+ end
58
+
59
+ private
60
+
61
+ def add_model(method_name, *args)
62
+ if method_name =~ /\Aadd_(\w+)\Z/
63
+ model_name = $1
64
+ many = args[0].kind_of? Integer
65
+ end
66
+ if model_name
67
+ grafter = Grafter.new(self, (many ? model_name.singularize : model_name))
68
+ if many
69
+ grafter.add_many(*args)
70
+ else
71
+ grafter.add_one(*args)
72
+ end
73
+ end
74
+ end
75
+
76
+ def get_model(method_name, *args)
77
+ if method_name =~ /\A(\w+?)(\d+)\Z/
78
+ model_name = $1
79
+ index = $2
80
+ elsif method_name =~ /\Athe_(\w+)\Z/
81
+ model_name = $1
82
+ index = 1
83
+ end
84
+ find(model_name, index)
85
+ end
86
+
87
+ def find(model_name, index)
88
+ model_name = model_name.to_sym
89
+ if index == 1 && get_default_for(model_name)
90
+ get_default_for(model_name)
91
+ elsif index == 1
92
+ @data[model_name] ||= []
93
+ @data[model_name].reject{|e| has_invalid_parent?(e)}.first
94
+ else
95
+ @data[model_name] ||= []
96
+ @data[model_name][index.to_i-1]
97
+ end
98
+ end
99
+
100
+ def get_default_for(model)
101
+ @bounding_models[model] || @current_default[model] || nil
102
+ end
103
+
104
+ def has_invalid_parent?(model)
105
+ @bounding_models.each do |k,v|
106
+ return true if (model.respond_to?(k) && model.send(k) != v)
107
+ end
108
+ false
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,6 @@
1
+ namespace :data_works do
2
+ desc "tell data_works that the model relationships are accurate"
3
+ task :bless => :environment do
4
+ DataWorks::StaleRelationshipChecker.create_snapshot!
5
+ end
6
+ end
@@ -0,0 +1,118 @@
1
+ require_relative "helper/data_works_spec_helper"
2
+
3
+ describe TheDataWorks do
4
+ let!(:data) { TheDataWorks.new }
5
+
6
+ describe 'adding a record with no necessary parents' do
7
+ describe "add_pet" do
8
+ it 'creates a Pet record' do
9
+ expect { data.add_pet }.to change(Pet, :count).by(1)
10
+ end
11
+ end
12
+
13
+ describe 'with factory traits' do
14
+ let(:put_a_bird_on_it) { data.add_pet_bird }
15
+
16
+ describe "add_pet_bird" do
17
+ it 'creates a Pet record' do
18
+ expect { put_a_bird_on_it }.to change(Pet, :count).by(1)
19
+ end
20
+ it 'creates a record with the required trait' do
21
+ the_bird = put_a_bird_on_it
22
+ expect( the_bird.kind ).to eq 'Bird'
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ describe 'adding a record with one necessary parent' do
29
+ describe 'add_pet_sitter' do
30
+ it 'creates a PetSitter record' do
31
+ expect { data.add_pet_sitter }.to change(PetSitter, :count).by(1)
32
+ end
33
+ it 'creates an Agency record' do
34
+ expect { data.add_pet_sitter }.to change(Agency, :count).by(1)
35
+ end
36
+ end
37
+
38
+ describe 'with factory traits' do
39
+ describe 'on the child model' do
40
+ let(:da_hoomans_toy) { data.add_hooman_toy }
41
+
42
+ describe "add_hooman_toy" do
43
+ it 'creates a Pet record' do # As it still should
44
+ expect { da_hoomans_toy }.to change(Pet, :count).by(1)
45
+ end
46
+ it 'creates a record with the required trait' do
47
+ robo_vac = da_hoomans_toy
48
+ expect( robo_vac.kind ).to eq 'Robot Vacuum' # add the correct trait
49
+ end
50
+ end
51
+ end
52
+
53
+ describe 'on the child and parent models' do
54
+ let(:a_tiny_bell) { data.add_bell_toy }
55
+
56
+ describe "add_bell_toy" do
57
+ it 'creates a Pet record' do # As it still should
58
+ expect { a_tiny_bell }.to change(Pet, :count).by(1)
59
+ end
60
+ it 'creates a record with the required trait on both models' do
61
+ the_bell = a_tiny_bell
62
+ the_bird = data.the_pet_bird
63
+ expect( the_bell.kind ).to eq 'Bell' # add the correct trait
64
+ expect( the_bird.kind ).to eq 'Bird' # add the correct trait
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ describe 'adding a record with multiple necessary parents' do
72
+ describe 'add_pet_sitting_patronage' do
73
+ it 'creates a PetSittingPatronage record' do
74
+ expect { data.add_pet_sitting_patronage }.to change(PetSittingPatronage, :count).by(1)
75
+ end
76
+ it 'creates a Pet record' do
77
+ expect { data.add_pet_sitting_patronage }.to change(Pet, :count).by(1)
78
+ end
79
+ it 'creates a PetSitter record' do
80
+ expect { data.add_pet_sitting_patronage }.to change(PetSitter, :count).by(1)
81
+ end
82
+ it 'creates an Agency record' do # necessary for PetSitter
83
+ expect { data.add_pet_sitting_patronage }.to change(Agency, :count).by(1)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe 'adding multiple records at once' do
89
+ describe 'add_pets(2)' do
90
+ it 'creates two Pet records' do
91
+ expect { data.add_pets(2) }.to change(Pet, :count).by(2)
92
+ end
93
+ end
94
+ describe 'add_pet_sitter(s)' do
95
+ it 'creates a PetSitter record' do
96
+ expect { data.add_pet_sitters(2) }.to change(PetSitter, :count).by(2)
97
+ end
98
+ it 'creates an Agency record' do
99
+ expect { data.add_pet_sitters(2) }.to change(Agency, :count).by(1)
100
+ end
101
+ end
102
+ end
103
+
104
+ describe 'adding a record with custom associations' do
105
+ describe 'add_picture' do
106
+ it 'creates a Picture record' do
107
+ expect { data.add_picture }.to change(Picture, :count).by(1)
108
+ end
109
+ it 'creates a Product record (polymorhpic)' do
110
+ expect { data.add_picture }.to change(Product, :count).by(1)
111
+ end
112
+ it 'creates an Album record (custom foreign key)' do
113
+ expect { data.add_picture }.to change(Album, :count).by(1)
114
+ end
115
+ end
116
+ end
117
+
118
+ end
@@ -0,0 +1,78 @@
1
+ require_relative '../lib/data_faker'
2
+
3
+ FactoryGirl.define do
4
+ factory :pet do
5
+ name { fake_string }
6
+ kind { animal_types.sample }
7
+ birth_year { (1914..2014).to_a.sample }
8
+
9
+ trait :bird do
10
+ kind 'Bird'
11
+ end
12
+
13
+ factory :pet_bird, traits: [:bird]
14
+ end
15
+
16
+ factory :agency do
17
+ name { fake_string }
18
+ end
19
+
20
+ factory :address do
21
+ street { fake_string + %w(Street Avenue Lane Road).sample }
22
+ city { fake_word.capitalize }
23
+ state { fake_word.capitalize }
24
+ end
25
+
26
+ factory :pet_food do
27
+ name { "#{animal_types.sample} #{%w(Kibble Chow Food Munchies).sample}" }
28
+ end
29
+
30
+ factory :pet_profile do
31
+ description { "#{fake_string} #{fake_string} #{fake_string}" }
32
+ end
33
+
34
+ factory :pet_sitter do
35
+ name { fake_string }
36
+ kind { Kind.all.sample }
37
+ end
38
+
39
+ factory :pet_sitting_patronage do
40
+ end
41
+
42
+ factory :tag do
43
+ registered_name { fake_string }
44
+ end
45
+
46
+ factory :toy do
47
+ name { "#{%w(Fluffy Rubber Squeeky).sample} #{%w(Ball Stick Shoe Book).sample}" }
48
+
49
+ trait :hooman do
50
+ kind 'Robot Vacuum' # you know the one
51
+ end
52
+
53
+ trait :bell do
54
+ kind 'Bell'
55
+ end
56
+
57
+ factory :hooman_toy, traits: [:hooman]
58
+ factory :bell_toy, traits: [:bell]
59
+ end
60
+
61
+ factory :kind do
62
+ name { fake_word }
63
+ end
64
+
65
+ factory :picture do
66
+ end
67
+
68
+ factory :album do
69
+ name { fake_word }
70
+ end
71
+
72
+ factory :product do
73
+ end
74
+ end
75
+
76
+ def animal_types
77
+ %w(Cat Dog Ferret Moose Octopus)
78
+ end