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