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