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.
- 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
|