neoscout 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rvmrc +2 -0
- data/AUTHORS +1 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/Rakefile +30 -0
- data/TODO.org +27 -0
- data/etc/neo4j.yml +16 -0
- data/lib/neoscout.rb +12 -0
- data/lib/neoscout/constraints.rb +35 -0
- data/lib/neoscout/gdb_neo4j.rb +147 -0
- data/lib/neoscout/json_schema.rb +136 -0
- data/lib/neoscout/main.rb +205 -0
- data/lib/neoscout/model.rb +148 -0
- data/lib/neoscout/scout.rb +119 -0
- data/lib/neoscout/tools.rb +156 -0
- data/lib/neoscout/version.rb +3 -0
- data/neoscout.gemspec +25 -0
- data/root/README.md +3 -0
- data/script/neoscout +15 -0
- data/spec/lib/neoscout/constraints_spec.rb +25 -0
- data/spec/lib/neoscout/gdb_neo4j_spec.rb +81 -0
- data/spec/lib/neoscout/gdb_neo4j_spec_counts.json +282 -0
- data/spec/lib/neoscout/gdb_neo4j_spec_schema.json +46 -0
- data/spec/lib/neoscout/model_spec.rb +42 -0
- data/spec/lib/neoscout/tools_spec.rb +139 -0
- data/spec/spec_helper.rb +5 -0
- metadata +84 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module NeoScout
|
4
|
+
|
5
|
+
class ElementIterator
|
6
|
+
def iter_nodes(args) ; raise NotImplentedError end
|
7
|
+
def iter_edges(args) ; raise NotImplentedError end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Typer
|
11
|
+
def node_type(node) ; raise NotImplementedError end
|
12
|
+
def edge_type(edge) ; raise NotImplementedError end
|
13
|
+
|
14
|
+
def checked_node_type?(node_type) ; raise NotImplementedError end
|
15
|
+
def checked_edge_type?(edge_type) ; raise NotImplementedError end
|
16
|
+
|
17
|
+
def valid_value?(value_type, value) ; true end
|
18
|
+
|
19
|
+
def unknown_node_type?(type) ; false end
|
20
|
+
def unknown_edge_type?(type) ; false end
|
21
|
+
end
|
22
|
+
|
23
|
+
module TyperValueTableMixin
|
24
|
+
def valid_value?(value_type, value)
|
25
|
+
if (entry = self.value_type_table[value_type])
|
26
|
+
entry.call(value_type_name, value)
|
27
|
+
else
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Counts
|
34
|
+
attr_reader :all_nodes
|
35
|
+
attr_reader :all_edges
|
36
|
+
|
37
|
+
attr_reader :typed_nodes
|
38
|
+
attr_reader :typed_edges
|
39
|
+
|
40
|
+
attr_reader :typed_node_props
|
41
|
+
attr_reader :typed_edge_props
|
42
|
+
|
43
|
+
attr_reader :node_link_src_stats
|
44
|
+
attr_reader :node_link_dst_stats
|
45
|
+
attr_reader :edge_link_src_stats
|
46
|
+
attr_reader :edge_link_dst_stats
|
47
|
+
|
48
|
+
def initialize(typer)
|
49
|
+
@typer = typer
|
50
|
+
reset
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset
|
54
|
+
@all_nodes = Counter.new
|
55
|
+
@all_edges = Counter.new
|
56
|
+
|
57
|
+
@typed_nodes = Counter.new_multi_keyed :node_type
|
58
|
+
@typed_edges = Counter.new_multi_keyed :edge_type
|
59
|
+
|
60
|
+
@typed_node_props = Counter.new_multi_keyed :node_type, :prop_constr
|
61
|
+
@typed_edge_props = Counter.new_multi_keyed :node_type, :prop_constr
|
62
|
+
|
63
|
+
@node_link_src_stats = Counter.new_multi_keyed :src_type, :edge_type
|
64
|
+
@node_link_dst_stats = Counter.new_multi_keyed :dst_type, :edge_type
|
65
|
+
|
66
|
+
@edge_link_src_stats = Counter.new_multi_keyed :edge_type, :src_type, :dst_type
|
67
|
+
@edge_link_dst_stats = Counter.new_multi_keyed :edge_type, :dst_type, :src_type
|
68
|
+
end
|
69
|
+
|
70
|
+
def count_node(type, ok)
|
71
|
+
@all_nodes.incr(ok)
|
72
|
+
@typed_nodes[type].incr(ok)
|
73
|
+
end
|
74
|
+
|
75
|
+
def count_node_prop(type, prop, ok)
|
76
|
+
@typed_node_props[type][prop].incr(ok)
|
77
|
+
end
|
78
|
+
|
79
|
+
def count_edge(type, ok)
|
80
|
+
@all_edges.incr(ok)
|
81
|
+
@typed_edges[type].incr(ok)
|
82
|
+
end
|
83
|
+
|
84
|
+
def count_edge_prop(type, prop, ok)
|
85
|
+
@typed_edge_props[type][prop].incr(ok)
|
86
|
+
end
|
87
|
+
|
88
|
+
def count_link_stats(edge_type, src_type, dst_type, ok)
|
89
|
+
# puts "#{src_type} -- #{edge_type} -- #{dst_type} #{if ok then "CHECK" else "FAIL" end}"
|
90
|
+
@node_link_src_stats[src_type][edge_type].incr(ok)
|
91
|
+
@node_link_dst_stats[dst_type][edge_type].incr(ok)
|
92
|
+
@edge_link_src_stats[edge_type][src_type][dst_type].incr(ok)
|
93
|
+
@edge_link_dst_stats[edge_type][dst_type][src_type].incr(ok)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
class Verifier
|
99
|
+
attr_reader :node_props
|
100
|
+
attr_reader :edge_props
|
101
|
+
attr_reader :allowed_edges
|
102
|
+
|
103
|
+
def initialize
|
104
|
+
@node_props = HashWithDefault.new { |type| ConstrainedSet.new { |o| o.kind_of? Constraints::PropConstraint } }
|
105
|
+
@edge_props = HashWithDefault.new { |type| ConstrainedSet.new { |o| o.kind_of? Constraints::PropConstraint } }
|
106
|
+
@allowed_edges = HashWithDefault.new_multi_keyed(:edge_type, :src_type) { |v| Set.new }
|
107
|
+
end
|
108
|
+
|
109
|
+
def new_node_prop_constr(args={})
|
110
|
+
Constraints::PropConstraint.new args
|
111
|
+
end
|
112
|
+
|
113
|
+
def new_edge_prop_constr(args={})
|
114
|
+
Constraints::PropConstraint.new args
|
115
|
+
end
|
116
|
+
|
117
|
+
def new_card_constr(args={})
|
118
|
+
Constraints::CardConstraint.new args
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_valid_edge(edge_type, src_type, dst_type)
|
122
|
+
@allowed_edges[edge_type][src_type] << dst_type
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_valid_edge_sets(edge_type, src_types, dst_types)
|
126
|
+
src_types.each do |src_type|
|
127
|
+
dst_types.each do |dst_type|
|
128
|
+
add_valid_edge edge_type, src_type, dst_type
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def checked_node_type?(node_type)
|
134
|
+
! @node_props[node_type].empty?
|
135
|
+
end
|
136
|
+
|
137
|
+
def checked_edge_type?(node_type)
|
138
|
+
! @node_props[node_type].empty?
|
139
|
+
end
|
140
|
+
|
141
|
+
def allowed_edge?(edge_type, src_type, dst_type)
|
142
|
+
allowed_edges[edge_type].empty? || allowed_edges[edge_type][src_type].member?(dst_type)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module NeoScout
|
2
|
+
|
3
|
+
class Scout
|
4
|
+
attr_reader :typer, :verifier, :iterator
|
5
|
+
|
6
|
+
def initialize(args={})
|
7
|
+
@typer = args[:typer]
|
8
|
+
@typer = Typer.new unless @typer
|
9
|
+
@verifier = args[:verifier]
|
10
|
+
@verifier = Verifier.new unless @verifier
|
11
|
+
@iterator = args[:iterator]
|
12
|
+
@iterator = ElementIterator.new unless @iterator
|
13
|
+
end
|
14
|
+
|
15
|
+
def checked_node_type?(node_type)
|
16
|
+
@typer.checked_node_type?(node_type) && @verifier.checked_node_type?(node_type)
|
17
|
+
end
|
18
|
+
|
19
|
+
def checked_edge_type?(edge_type)
|
20
|
+
@typer.checked_edge_type?(edge_type) && @verifier.checked_edge_type?(edge_type)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def new_counts
|
25
|
+
NeoScout::Counts.new(typer)
|
26
|
+
end
|
27
|
+
|
28
|
+
def count_nodes(args)
|
29
|
+
counts = prep_counts(args[:counts])
|
30
|
+
@iterator.iter_nodes(args) do |node|
|
31
|
+
begin
|
32
|
+
node_type = @typer.node_type(node)
|
33
|
+
node_ok = process_node(counts, node_type, node)
|
34
|
+
counts.count_node(node_type, node_ok)
|
35
|
+
rescue Exception => e
|
36
|
+
puts e
|
37
|
+
counts.count_node(e.class.to_s, false)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
counts
|
41
|
+
end
|
42
|
+
|
43
|
+
def count_edges(args)
|
44
|
+
counts = prep_counts(args[:counts])
|
45
|
+
@iterator.iter_edges(args) do |edge|
|
46
|
+
begin
|
47
|
+
edge_type = @typer.edge_type(edge)
|
48
|
+
edge_ok = process_edge(counts, edge_type, edge)
|
49
|
+
counts.count_edge(edge_type, edge_ok)
|
50
|
+
rescue Exception => e
|
51
|
+
puts e
|
52
|
+
counts.count_edge(e.class.to_s, false)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
counts
|
56
|
+
end
|
57
|
+
|
58
|
+
def prep_counts(counts) ; counts end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def process_node(counts, node_type, node)
|
63
|
+
node_ok = true
|
64
|
+
|
65
|
+
node_props = Set.new(node.props.keys)
|
66
|
+
|
67
|
+
node_props.delete('_neo_id')
|
68
|
+
|
69
|
+
@verifier.node_props[node_type].each do |constr|
|
70
|
+
prop_ok = constr.satisfied_by_node?(typer, node)
|
71
|
+
counts.count_node_prop(node_type, constr.name, prop_ok)
|
72
|
+
node_props.delete(constr.name)
|
73
|
+
node_ok &&= prop_ok
|
74
|
+
end
|
75
|
+
|
76
|
+
# Process remaining properties in this node as erroneously missing in the schema
|
77
|
+
# unless the node is untyped
|
78
|
+
node_props.each do |prop_name|
|
79
|
+
prop_ok = ! checked_node_type?(node_type)
|
80
|
+
counts.count_node_prop(node_type, prop_name, prop_ok)
|
81
|
+
node_ok &&= prop_ok
|
82
|
+
end
|
83
|
+
|
84
|
+
node_ok
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_edge(counts, edge_type, edge)
|
88
|
+
edge_props = Set.new(edge.props.keys)
|
89
|
+
edge_props.delete('_neo_id')
|
90
|
+
|
91
|
+
src_type = @typer.node_type(edge.getStartNode)
|
92
|
+
dst_type = @typer.node_type(edge.getEndNode)
|
93
|
+
|
94
|
+
edge_ok = @verifier.allowed_edge?(edge_type, src_type, dst_type)
|
95
|
+
|
96
|
+
@verifier.edge_props[edge_type].each do |constr|
|
97
|
+
prop_ok = constr.satisfied_by_edge?(typer, edge)
|
98
|
+
counts.count_edge_prop(edge_type, constr.name, prop_ok)
|
99
|
+
edge_props.delete(constr.name)
|
100
|
+
edge_ok &&= prop_ok
|
101
|
+
end
|
102
|
+
|
103
|
+
# Process remaining properties in this node as erroneously missing in the schema
|
104
|
+
# unless the edge is untyped
|
105
|
+
edge_props.each do |prop_name|
|
106
|
+
prop_ok = ! checked_edge_type?(edge_type)
|
107
|
+
counts.count_edge_prop(edge_type, prop_name, prop_ok)
|
108
|
+
edge_ok &&= prop_ok
|
109
|
+
end
|
110
|
+
|
111
|
+
# Finally count edge statistics
|
112
|
+
counts.count_link_stats(edge_type, src_type, dst_type, edge_ok)
|
113
|
+
|
114
|
+
edge_ok
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module NeoScout
|
2
|
+
|
3
|
+
class Counter
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset
|
7
|
+
end
|
8
|
+
|
9
|
+
def reset
|
10
|
+
@ok = 0
|
11
|
+
@total = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def incr(ok)
|
15
|
+
if ok then incr_ok else incr_failed end
|
16
|
+
end
|
17
|
+
|
18
|
+
def incr_ok
|
19
|
+
@ok += 1
|
20
|
+
@total += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def incr_failed
|
24
|
+
@total +=1
|
25
|
+
end
|
26
|
+
|
27
|
+
def num_ok
|
28
|
+
@ok
|
29
|
+
end
|
30
|
+
|
31
|
+
def num_failed
|
32
|
+
@total - @ok
|
33
|
+
end
|
34
|
+
|
35
|
+
def num_total
|
36
|
+
@total
|
37
|
+
end
|
38
|
+
|
39
|
+
def empty?
|
40
|
+
@total == 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
"(#{num_ok}/#{num_failed}/#{num_total})"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class ConstrainedSet < Set
|
50
|
+
|
51
|
+
def initialize(*args, &elem_test)
|
52
|
+
@elem_test = elem_test
|
53
|
+
case
|
54
|
+
when args.length == 0
|
55
|
+
super
|
56
|
+
when args.length == 1
|
57
|
+
args = args[0]
|
58
|
+
raise ArgumentError unless (args.all? &@elem_test)
|
59
|
+
super args
|
60
|
+
else
|
61
|
+
raise ArgumentError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid_elem?(elem)
|
66
|
+
@elem_test.call(elem)
|
67
|
+
end
|
68
|
+
|
69
|
+
def <<(elem)
|
70
|
+
raise ArgumentError unless valid_elem?(elem)
|
71
|
+
super elem
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
module HashDefaultsMixin
|
77
|
+
|
78
|
+
def initialize(*args, &blk)
|
79
|
+
super *args
|
80
|
+
@default = blk
|
81
|
+
end
|
82
|
+
|
83
|
+
def default(key)
|
84
|
+
@default.call(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
def [](key)
|
88
|
+
if has_key?(key) then super(key) else self[key]=default(key) end
|
89
|
+
end
|
90
|
+
|
91
|
+
def lookup(key, default_value = nil)
|
92
|
+
if has_key?(key) then self[key] else self[key]=default_value end
|
93
|
+
end
|
94
|
+
|
95
|
+
def key_descr
|
96
|
+
:key
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def self.included(base)
|
101
|
+
|
102
|
+
# defines map_value for mixin target baseclass instances and any subclass instances
|
103
|
+
base.class_exec(base) do |base_class|
|
104
|
+
define_method(:map_value) do |&blk|
|
105
|
+
new_hash = {}
|
106
|
+
each_pair do |k,v|
|
107
|
+
new_hash[k] = if v.kind_of? base_class then v.map_value(&blk) else blk.call(v) end
|
108
|
+
end
|
109
|
+
new_hash
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# defines new_multi_keyed on the mixin's target baseclass
|
114
|
+
# (subclasses the baseclass to override key_descr for instances)
|
115
|
+
def base.new_multi_keyed(*list, &blk)
|
116
|
+
new_class = Class.new(self)
|
117
|
+
(class << new_class ; self end).class_exec(list.shift) do |descr|
|
118
|
+
define_method(:key_descr) { || descr }
|
119
|
+
end
|
120
|
+
if list.empty?
|
121
|
+
then new_class.new(&blk)
|
122
|
+
else new_class.new { |key| self.new_multi_keyed(*list, &blk) } end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class HashWithDefault < Hash
|
129
|
+
|
130
|
+
include HashDefaultsMixin
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
class Counter
|
135
|
+
|
136
|
+
def self.new_multi_keyed(*list)
|
137
|
+
HashWithDefault.new_multi_keyed(*list) { |key| Counter.new }
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
module JSON
|
143
|
+
|
144
|
+
def self.cd(json, args)
|
145
|
+
current = json
|
146
|
+
args.each do |k|
|
147
|
+
current = if current.has_key? k
|
148
|
+
then current[k]
|
149
|
+
else current[k] = {} end
|
150
|
+
end
|
151
|
+
current
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
data/neoscout.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require 'neoscout/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'neoscout'
|
7
|
+
s.version = NeoScout::VERSION
|
8
|
+
s.summary = 'Graph database schema extraction and validation tool'
|
9
|
+
s.description = 'Tool for validating the schema of a free form graph databases and for reporting errors, including a REST access layer for runtime checking'
|
10
|
+
s.author = 'Stefan Plantikow'
|
11
|
+
s.email = 'stefanp@moviepilot.com'
|
12
|
+
s.homepage = 'http://moviepilot.github.com/neoscout'
|
13
|
+
s.rubyforge_project = 'neoscout'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.bindir = 'script'
|
21
|
+
s.executables = `git ls-files -- script/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.default_executable = 'neoscout'
|
23
|
+
s.executables = ['neoscout']
|
24
|
+
s.licenses = ['PUBLIC DOMAIN WITHOUT ANY WARRANTY']
|
25
|
+
end
|