gexf 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -0
- data/lib/gexf.rb +21 -0
- data/lib/gexf/attribute.rb +92 -0
- data/lib/gexf/attribute/assignable.rb +81 -0
- data/lib/gexf/attribute/definable.rb +20 -0
- data/lib/gexf/document.rb +112 -0
- data/lib/gexf/edge.rb +73 -0
- data/lib/gexf/edgeset.rb +7 -0
- data/lib/gexf/graph.rb +82 -0
- data/lib/gexf/node.rb +52 -0
- data/lib/gexf/nodeset.rb +19 -0
- data/lib/gexf/set_of_sets.rb +38 -0
- data/lib/gexf/support.rb +18 -0
- data/lib/gexf/version.rb +3 -0
- data/lib/gexf/xml_serializer.rb +93 -0
- data/spec/gexf/attribute/assignable_spec.rb +118 -0
- data/spec/gexf/attribute/definable_spec.rb +66 -0
- data/spec/gexf/attribute_spec.rb +179 -0
- data/spec/gexf/edge_spec.rb +70 -0
- data/spec/gexf/edgeset_spec.rb +105 -0
- data/spec/gexf/graph_spec.rb +172 -0
- data/spec/gexf/node_spec.rb +105 -0
- data/spec/gexf/nodeset_spec.rb +4 -0
- data/spec/spec_helper.rb +2 -0
- metadata +125 -0
data/.gitignore
ADDED
File without changes
|
data/lib/gexf.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path('../gexf', __FILE__)
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module GEXF;end
|
8
|
+
|
9
|
+
require 'version'
|
10
|
+
require 'attribute'
|
11
|
+
require 'attribute/definable'
|
12
|
+
require 'attribute/assignable'
|
13
|
+
require 'set_of_sets'
|
14
|
+
require 'node'
|
15
|
+
require 'nodeset'
|
16
|
+
require 'edge'
|
17
|
+
require 'edgeset'
|
18
|
+
require 'graph'
|
19
|
+
require 'xml_serializer'
|
20
|
+
require 'document'
|
21
|
+
require 'support'
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class GEXF::Attribute
|
2
|
+
|
3
|
+
BOOLEAN = :boolean
|
4
|
+
STRING = :string
|
5
|
+
INTEGER = :integer
|
6
|
+
FLOAT = :float
|
7
|
+
ANY_URI = :anyURI
|
8
|
+
LIST_STRING = :liststring
|
9
|
+
TYPES = [ BOOLEAN, STRING, INTEGER, FLOAT, ANY_URI, LIST_STRING]
|
10
|
+
|
11
|
+
DYNAMIC = :dynamic
|
12
|
+
STATIC = :static
|
13
|
+
MODES = [DYNAMIC, STATIC]
|
14
|
+
|
15
|
+
NODE = :node
|
16
|
+
EDGE = :edge
|
17
|
+
CLASSES = [NODE, EDGE]
|
18
|
+
|
19
|
+
attr_reader :type, :id, :title, :options, :mode, :attr_class, :default
|
20
|
+
|
21
|
+
def initialize(id, title, opts={})
|
22
|
+
|
23
|
+
attr_class = opts[:class] || NODE
|
24
|
+
mode = opts[:mode] || STATIC
|
25
|
+
type = opts[:type] || STRING
|
26
|
+
default = opts[:default]
|
27
|
+
options = opts[:options]
|
28
|
+
id = id.to_s
|
29
|
+
|
30
|
+
@options = if type == LIST_STRING && options.respond_to?(:split)
|
31
|
+
options.split('|').uniq
|
32
|
+
else
|
33
|
+
Array(options).uniq
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
raise ArgumentError.new "Invalid or missing type: #{type}" if !TYPES.include?(type)
|
38
|
+
raise ArgumentError.new "invalid or missing class: #{attr_class}" if !CLASSES.include?(attr_class)
|
39
|
+
raise ArgumentError.new "Invalid mode: #{mode}" if !MODES.include?(mode)
|
40
|
+
|
41
|
+
@attr_class = attr_class
|
42
|
+
@type = type
|
43
|
+
@id = id
|
44
|
+
@type = type
|
45
|
+
@mode = mode
|
46
|
+
@title = title.to_s
|
47
|
+
self.default=default
|
48
|
+
end
|
49
|
+
|
50
|
+
def default=(value)
|
51
|
+
raise ArgumentError.new "value value '#{value}' is not included in 'options' list" if value && @options.any? && !@options.include?(value)
|
52
|
+
@default=value
|
53
|
+
end
|
54
|
+
|
55
|
+
#Note: is this a violation of the "Tell don't ask principle"?
|
56
|
+
def coherce(value)
|
57
|
+
case @type
|
58
|
+
when BOOLEAN
|
59
|
+
case value
|
60
|
+
when *['1', 'true', 1, true]
|
61
|
+
true
|
62
|
+
when *['0', 'false', 0, false]
|
63
|
+
false
|
64
|
+
end
|
65
|
+
when STRING, ANY_URI
|
66
|
+
value.to_s
|
67
|
+
when FLOAT
|
68
|
+
value.to_f if value.respond_to?(:to_f)
|
69
|
+
when INTEGER
|
70
|
+
value.to_i if value.respond_to?(:to_i)
|
71
|
+
|
72
|
+
when LIST_STRING
|
73
|
+
Array(value).flatten.map(&:to_s).uniq
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def is_valid?(value)
|
78
|
+
if @options.empty?
|
79
|
+
true
|
80
|
+
else
|
81
|
+
value = value.first if value.respond_to?(:first)
|
82
|
+
@options.map(&:to_s).include?(value.to_s)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_hash
|
87
|
+
optional = {}
|
88
|
+
optional[:options] = options.join('|') if options && options.any?
|
89
|
+
|
90
|
+
{:id => id, :title => title, :type => type}.merge(optional)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module GEXF::Attribute::Assignable
|
2
|
+
|
3
|
+
# Delegates calls for the 'defined_attributes' method to the @collection instance variable
|
4
|
+
#
|
5
|
+
# Returns nothing.
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(Forwardable) unless base.ancestors.include?(Forwardable)
|
9
|
+
|
10
|
+
base.def_delegator :collection, :attribute_definitions, :defined_attributes
|
11
|
+
base.def_delegator :attr_values, :[], :attr_value
|
12
|
+
end
|
13
|
+
|
14
|
+
# Reconstructs a hash of attiribute titles/values for the current node/edge
|
15
|
+
#
|
16
|
+
# Example:
|
17
|
+
#
|
18
|
+
# node.attributes
|
19
|
+
# => {:site => 'http://www.archive.org', :name => 'The internet archive'}
|
20
|
+
#
|
21
|
+
# Returns
|
22
|
+
#
|
23
|
+
# The attributes hash
|
24
|
+
#
|
25
|
+
def attributes()
|
26
|
+
Hash[*defined_attributes.map do |_, attr|
|
27
|
+
[attr.title, attr_value(attr.id)]
|
28
|
+
end.flatten]
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Fetches the attribute hash, and sets it to an empty array if this is not defined.
|
33
|
+
#
|
34
|
+
# Returns the 'attr_values' hash.
|
35
|
+
#
|
36
|
+
|
37
|
+
def attr_values
|
38
|
+
@attr_values ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Fetches an attribute by title.
|
43
|
+
#
|
44
|
+
# Returns the attribute value.
|
45
|
+
# Raises a warning if the attribute has not been defined..
|
46
|
+
#
|
47
|
+
|
48
|
+
def [](key)
|
49
|
+
if attr = attribute_by_title(key)
|
50
|
+
attr_value(attr.id) || attr.default
|
51
|
+
else
|
52
|
+
Kernel.warn "undefined attribute '#{key}'"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# low level setter, suitable to be used when parsing (see GEXF::Document)
|
57
|
+
def set_attr_by_id(attr_id, value)
|
58
|
+
@attr_values[attr_id] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
def []=(key, value)
|
62
|
+
attr = attribute_by_title(key)
|
63
|
+
value = attr && attr.coherce(value) || value
|
64
|
+
|
65
|
+
if attr && attr.is_valid?(value)
|
66
|
+
attr_values[attr.id] = value
|
67
|
+
else
|
68
|
+
Kernel.warn "undefined attribute '#{key}'"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def collection
|
74
|
+
@collection || []
|
75
|
+
end
|
76
|
+
|
77
|
+
def attribute_by_title(key)
|
78
|
+
_, attribute = *defined_attributes.find { |id, attr| attr.title == key.to_s }
|
79
|
+
attribute
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GEXF::Attribute::Definable
|
2
|
+
|
3
|
+
def define_attribute(id, title, opts={})
|
4
|
+
@attribute_definitions ||= {}
|
5
|
+
@attribute_definitions[id] = GEXF::Attribute.new(id, title, opts)
|
6
|
+
end
|
7
|
+
|
8
|
+
def attributes
|
9
|
+
Hash[*map do |item|
|
10
|
+
attributes = item.attributes
|
11
|
+
[item.id, attributes] if attributes && attributes.any?
|
12
|
+
end.compact.flatten]
|
13
|
+
end
|
14
|
+
|
15
|
+
def attribute_definitions
|
16
|
+
@attribute_definitions ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :defined_attributes, :attribute_definitions
|
20
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
class GEXF::Document < Nokogiri::XML::SAX::Document
|
2
|
+
|
3
|
+
attr_reader :graph, :meta
|
4
|
+
|
5
|
+
def start_document
|
6
|
+
@graph = nil
|
7
|
+
@node = nil
|
8
|
+
@edge = nil
|
9
|
+
@attr_class = nil
|
10
|
+
@attr = nil
|
11
|
+
@defined_attrs = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_element(tagname, attributes)
|
15
|
+
@current_tag = tagname
|
16
|
+
@current_tag_attrs = attributes
|
17
|
+
|
18
|
+
dispatch_event(tagname, sanitize_attrs(attributes))
|
19
|
+
end
|
20
|
+
|
21
|
+
def end_element(tagname)
|
22
|
+
case tagname
|
23
|
+
when 'attributes'
|
24
|
+
@attr_class = nil
|
25
|
+
when 'attribute'
|
26
|
+
@attr = nil
|
27
|
+
when 'node'
|
28
|
+
@node = nil
|
29
|
+
when 'edge'
|
30
|
+
@edge = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
@current_tag_attrs = {}
|
34
|
+
@current_tag = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def characters(chars)
|
38
|
+
chars = chars.strip
|
39
|
+
case @current_tag
|
40
|
+
when 'default'
|
41
|
+
@attr.default = chars
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def dispatch_event(tagname, attributes)
|
47
|
+
case tagname
|
48
|
+
when 'graph'
|
49
|
+
on_graph_start(attributes)
|
50
|
+
when 'attributes'
|
51
|
+
@attr_class = attributes[:class]
|
52
|
+
when 'attribute'
|
53
|
+
on_attribute_start(attributes)
|
54
|
+
when 'attvalue'
|
55
|
+
on_attrvalue_start(attributes)
|
56
|
+
when 'node'
|
57
|
+
on_node_start(attributes)
|
58
|
+
when 'edge'
|
59
|
+
on_edge_start(attributes)
|
60
|
+
else
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def sanitize_attrs(attributes)
|
65
|
+
Hash[*attributes.flatten].
|
66
|
+
symbolize_keys.
|
67
|
+
symbolize_graph_types
|
68
|
+
end
|
69
|
+
|
70
|
+
def on_graph_start(attributes)
|
71
|
+
@graph = GEXF::Graph.new(attributes)
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_attribute_start(definition)
|
75
|
+
title = definition.delete(:title)
|
76
|
+
type = definition[:type]
|
77
|
+
definition.delete(:class)
|
78
|
+
|
79
|
+
@attr = case @attr_class
|
80
|
+
when 'node'
|
81
|
+
graph.define_node_attribute(title, definition)
|
82
|
+
when 'edge'
|
83
|
+
graph.define_edge_attribute(title, definition)
|
84
|
+
end
|
85
|
+
|
86
|
+
@defined_attrs[@attr.id] = @attr.title
|
87
|
+
end
|
88
|
+
|
89
|
+
def on_node_start(attributes)
|
90
|
+
@node = graph.create_node(attributes)
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_edge_start(attributes)
|
94
|
+
source_id = attributes.delete(:source)
|
95
|
+
target_id = attributes.delete(:target)
|
96
|
+
|
97
|
+
source = graph.nodes[source_id]
|
98
|
+
target = graph.nodes[target_id]
|
99
|
+
|
100
|
+
@edge = graph.create_edge(source, target, attributes)
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_attrvalue_start(attrs)
|
104
|
+
attr_id = attrs[:for]
|
105
|
+
|
106
|
+
if @defined_attrs[attr_id]
|
107
|
+
@node.set_attr_by_id(attr_id, attrs[:value])
|
108
|
+
else
|
109
|
+
warn "Cannot find an attribute with id: #{id}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/gexf/edge.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
class GEXF::Edge
|
2
|
+
include GEXF::Attribute::Assignable
|
3
|
+
|
4
|
+
DIRECTED = :directed
|
5
|
+
UNDIRECTED = :undirected
|
6
|
+
MUTUAL = :mutual
|
7
|
+
|
8
|
+
TYPES = [ DIRECTED, UNDIRECTED, MUTUAL]
|
9
|
+
|
10
|
+
attr_reader :id, :source_id, :target_id, :weight, :type, :label
|
11
|
+
|
12
|
+
def initialize(id, source_id, target_id, opts={})
|
13
|
+
|
14
|
+
type = opts[:type] || UNDIRECTED
|
15
|
+
weight = opts[:weight] && opts[:weight].to_f
|
16
|
+
graph = opts[:graph]
|
17
|
+
label = opts[:label] && opts[:label].to_s || nil
|
18
|
+
id = id.to_s
|
19
|
+
source_id = source_id.to_s
|
20
|
+
target_id = target_id.to_s
|
21
|
+
|
22
|
+
raise ArgumentError.new("Invalid type: #{type}") if !TYPES.include?(type)
|
23
|
+
raise ArgumentError.new("'weight' should be a positive, numerical value") if weight && weight < 0.1
|
24
|
+
raise ArgumentError.new("Missing graph") if !graph
|
25
|
+
raise ArgumentError.new("Missing id") if !id || id.empty?
|
26
|
+
raise ArgumentError.new("Missing source_id") if !source_id || source_id.empty?
|
27
|
+
raise ArgumentError.new("Missing target_id") if !target_id || target_id.empty?
|
28
|
+
|
29
|
+
@id = id.to_s
|
30
|
+
@label = label
|
31
|
+
@type = type
|
32
|
+
@source_id = source_id.to_s
|
33
|
+
@target_id = target_id.to_s
|
34
|
+
@weight = weight || 1.0
|
35
|
+
@graph = graph
|
36
|
+
# see GEXF::Attribute::Assignable
|
37
|
+
@collection = @graph.edges
|
38
|
+
@attr_values = {}
|
39
|
+
|
40
|
+
set_attributes(opts.fetch(:attributes, {}))
|
41
|
+
end
|
42
|
+
|
43
|
+
[:directed, :undirected, :mutual].each do |type|
|
44
|
+
define_method("#{type}?") do
|
45
|
+
@type == self.class.const_get(type.to_s.upcase)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def target
|
50
|
+
@graph.nodes[target_id]
|
51
|
+
end
|
52
|
+
|
53
|
+
def source
|
54
|
+
@graph.nodes[source_id]
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_hash
|
58
|
+
optional = {}
|
59
|
+
optional[:label] = label if label && !label.empty?
|
60
|
+
|
61
|
+
{:id => id,
|
62
|
+
:source => source_id,
|
63
|
+
:target => target_id,
|
64
|
+
:type => type
|
65
|
+
|
66
|
+
}.merge(optional)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def set_attributes(attributes={})
|
71
|
+
attributes.each { |attr,value| self[attr]=value }
|
72
|
+
end
|
73
|
+
end
|
data/lib/gexf/edgeset.rb
ADDED
data/lib/gexf/graph.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
class GEXF::Graph
|
2
|
+
|
3
|
+
STRING = :string
|
4
|
+
INTEGER = :integer
|
5
|
+
IDTYPES = [STRING, INTEGER]
|
6
|
+
|
7
|
+
STATIC = :static
|
8
|
+
DYNAMIC = :dynamic
|
9
|
+
MODES = [STATIC, DYNAMIC]
|
10
|
+
|
11
|
+
EDGETYPES = GEXF::Edge::TYPES
|
12
|
+
|
13
|
+
attr_reader :defaultedgetype, :idtype, :mode, :nodes, :edges
|
14
|
+
|
15
|
+
def initialize(opts={})
|
16
|
+
edgetype = opts[:defaultedgetype] || GEXF::Edge::UNDIRECTED
|
17
|
+
idtype = opts[:idtype] || STRING
|
18
|
+
mode = opts[:mode] || STATIC
|
19
|
+
id_counter = Struct.new(:nodes, :edges, :attributes)
|
20
|
+
|
21
|
+
raise ArgumentError.new "Invalid defaultedgetype: '#{edgetype}'" unless EDGETYPES.include?(edgetype)
|
22
|
+
raise ArgumentError.new "Invalid idtype: '#{idtype}'" unless IDTYPES.include?(idtype)
|
23
|
+
raise ArgumentError.new "Invalid mode: '#{mode}'" unless MODES.include?(mode)
|
24
|
+
|
25
|
+
@defaultedgetype = edgetype
|
26
|
+
@idtype = idtype
|
27
|
+
@mode = mode
|
28
|
+
@id_counter = id_counter.new(0,0,0)
|
29
|
+
@attributes = {}
|
30
|
+
|
31
|
+
@nodes = GEXF::NodeSet.new
|
32
|
+
@edges = GEXF::EdgeSet.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def defined_attributes
|
36
|
+
nodes.defined_attributes.merge(edges.defined_attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
def define_node_attribute(title, opts={})
|
40
|
+
id = assign_id(:attributes, opts.delete(:id)).to_s
|
41
|
+
@nodes.define_attribute(id, title, opts)
|
42
|
+
end
|
43
|
+
|
44
|
+
def define_edge_attribute(title, opts={})
|
45
|
+
id = assign_id(:attributes, opts.delete(:id)).to_s
|
46
|
+
@edges.define_attribute(id, title, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_node(opts={})
|
50
|
+
id = assign_id(:nodes, opts.delete(:id))
|
51
|
+
@nodes << node = GEXF::Node.new(id, self, opts)
|
52
|
+
node
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_edge(source, target, opts={})
|
56
|
+
opts[:type] ||= defaultedgetype
|
57
|
+
id = assign_id(:edges, opts.delete(:id))
|
58
|
+
@edges << edge = GEXF::Edge.new(id, source.id, target.id, opts.merge(:graph => self))
|
59
|
+
edge
|
60
|
+
end
|
61
|
+
|
62
|
+
def attribute_definitions
|
63
|
+
@attributes.dup
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def assign_id(counter_name, id=nil)
|
68
|
+
auto_id = @id_counter.send(counter_name) + 1
|
69
|
+
|
70
|
+
if !id
|
71
|
+
id = auto_id
|
72
|
+
@id_counter.send("#{counter_name}=", id)
|
73
|
+
end
|
74
|
+
|
75
|
+
cast_id(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
def cast_id(id)
|
79
|
+
@idtype == INTEGER ? id.to_i : id.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|