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
@@ -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,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
|