neoscout 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module NeoScout
2
+ VERSION = '0.1'
3
+ 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