gexf 0.0.2
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 +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
|