redlander 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +59 -0
- data/Rakefile +5 -0
- data/ext/README +1 -0
- data/ext/extconf.rb +17 -0
- data/ext/redland-pre.i +6 -0
- data/ext/redland-types.i +9 -0
- data/ext/redland_wrap.c +8052 -0
- data/lib/redlander.rb +61 -0
- data/lib/redlander/error_container.rb +41 -0
- data/lib/redlander/model.rb +33 -0
- data/lib/redlander/model_proxy.rb +85 -0
- data/lib/redlander/node.rb +90 -0
- data/lib/redlander/parser.rb +92 -0
- data/lib/redlander/parser_proxy.rb +18 -0
- data/lib/redlander/serializer.rb +80 -0
- data/lib/redlander/statement.rb +153 -0
- data/lib/redlander/statement_iterator.rb +37 -0
- data/lib/redlander/storage.rb +79 -0
- data/lib/redlander/version.rb +3 -0
- data/spec/redlander/model_spec.rb +239 -0
- data/spec/redlander/node_spec.rb +85 -0
- data/spec/redlander/parser_spec.rb +96 -0
- data/spec/redlander/serializer_spec.rb +52 -0
- data/spec/redlander/statement_spec.rb +86 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +127 -0
data/lib/redlander.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
require 'xml_schema'
|
6
|
+
|
7
|
+
module Redlander
|
8
|
+
require 'redland'
|
9
|
+
require 'redlander/version'
|
10
|
+
|
11
|
+
class RedlandError < RuntimeError; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def rdf_world
|
16
|
+
unless @rdf_world
|
17
|
+
@rdf_world = Redland.librdf_new_world
|
18
|
+
raise RedlandError.new("Could not create a new RDF world") unless @rdf_world
|
19
|
+
ObjectSpace.define_finalizer(@rdf_world, proc { Redland.librdf_free_world(@rdf_world) })
|
20
|
+
Redland.librdf_world_open(@rdf_world)
|
21
|
+
end
|
22
|
+
@rdf_world
|
23
|
+
end
|
24
|
+
|
25
|
+
# Convert options hash into a string for librdf.
|
26
|
+
# What it does:
|
27
|
+
# 1) Convert boolean values into 'yes/no' values
|
28
|
+
# 2) Change underscores in key names into dashes ('dhar_ma' => 'dhar-ma')
|
29
|
+
# 3) Join all options as "key='value'" pairs in a comma-separated string
|
30
|
+
def to_rdf_options(options = {})
|
31
|
+
options.inject([]){|opts, option_pair|
|
32
|
+
key = option_pair[0].to_s.gsub(/_/, '-')
|
33
|
+
value = if [TrueClass, FalseClass].include?(option_pair[1].class)
|
34
|
+
option_pair[1] ? 'yes' : 'no'
|
35
|
+
else
|
36
|
+
option_pair[1]
|
37
|
+
end
|
38
|
+
opts << "#{key}='#{value}'"
|
39
|
+
}.join(',')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Helper method to create an instance of rdfuri.
|
43
|
+
# For internal use only!
|
44
|
+
def to_rdf_uri(uri)
|
45
|
+
return nil if uri.nil?
|
46
|
+
uri = uri.is_a?(URI) ? uri.to_s : uri
|
47
|
+
rdf_uri = Redland.librdf_new_uri(rdf_world, uri)
|
48
|
+
raise RedlandError.new("Failed to create URI from '#{uri}'") unless rdf_uri
|
49
|
+
ObjectSpace.define_finalizer(rdf_uri, proc { Redland.librdf_free_uri(rdf_uri) })
|
50
|
+
rdf_uri
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
require 'redlander/error_container'
|
56
|
+
require 'redlander/statement_iterator'
|
57
|
+
require 'redlander/parser'
|
58
|
+
require 'redlander/serializer'
|
59
|
+
require 'redlander/model'
|
60
|
+
require 'redlander/node'
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ErrorContainer
|
2
|
+
|
3
|
+
class Errors
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@container = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(error_message)
|
11
|
+
if @container.include?(error_message)
|
12
|
+
@container
|
13
|
+
else
|
14
|
+
@container << error_message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
@container.each do |err|
|
20
|
+
yield err
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
@container.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear
|
29
|
+
@container.clear
|
30
|
+
end
|
31
|
+
|
32
|
+
def size
|
33
|
+
@container.size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def errors
|
38
|
+
@errors ||= Errors.new
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'redlander/storage'
|
2
|
+
require 'redlander/model_proxy'
|
3
|
+
|
4
|
+
module Redlander
|
5
|
+
|
6
|
+
class Model
|
7
|
+
|
8
|
+
include Redlander::ParsingInstanceMethods
|
9
|
+
include Redlander::SerializingInstanceMethods
|
10
|
+
|
11
|
+
attr_reader :rdf_model
|
12
|
+
|
13
|
+
# Create a new RDF model.
|
14
|
+
# For explanation of options, read Storage.initialize_storage
|
15
|
+
def initialize(options = {})
|
16
|
+
@rdf_storage = Storage.initialize_storage(options)
|
17
|
+
|
18
|
+
@rdf_model = Redland.librdf_new_model(Redlander.rdf_world, @rdf_storage, "")
|
19
|
+
raise RedlandError.new("Failed to create a new model") unless @rdf_model
|
20
|
+
ObjectSpace.define_finalizer(@rdf_model, proc { Redland.librdf_free_model(@rdf_model) })
|
21
|
+
end
|
22
|
+
|
23
|
+
# Statements contained in the model.
|
24
|
+
#
|
25
|
+
# Similar to Ruby on Rails, a proxy object is actually returned,
|
26
|
+
# which delegates methods to Statement class.
|
27
|
+
def statements
|
28
|
+
ModelProxy.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'redlander/statement'
|
2
|
+
|
3
|
+
module Redlander
|
4
|
+
|
5
|
+
class ModelProxy
|
6
|
+
|
7
|
+
include StatementIterator
|
8
|
+
|
9
|
+
def initialize(model, rdf_stream = nil)
|
10
|
+
@model = model
|
11
|
+
@rdf_stream = if rdf_stream
|
12
|
+
rdf_stream
|
13
|
+
else
|
14
|
+
Redland.librdf_model_as_stream(@model.rdf_model)
|
15
|
+
end
|
16
|
+
raise RedlandError.new("Failed to create a new stream") unless @rdf_stream
|
17
|
+
ObjectSpace.define_finalizer(@rdf_stream, proc { Redland.librdf_free_stream(@rdf_stream) })
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a statement to the model.
|
21
|
+
# It must be a complete statement - all of subject, predicate, object parts must be present.
|
22
|
+
# Only statements that are legal RDF can be added.
|
23
|
+
# If the statement already exists in the model, it is not added.
|
24
|
+
#
|
25
|
+
# Returns true on success or false on failure.
|
26
|
+
def add(statement)
|
27
|
+
statement.model = @model
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a statement and add it to the model.
|
31
|
+
#
|
32
|
+
# Options are:
|
33
|
+
# :subject, :predicate, :object,
|
34
|
+
# (see Statement.new for option explanations).
|
35
|
+
#
|
36
|
+
# Returns an instance of Statement on success,
|
37
|
+
# or nil if the statement could not be added.
|
38
|
+
def create(options = {})
|
39
|
+
statement = Statement.new(options)
|
40
|
+
add(statement) && statement
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty?
|
44
|
+
size.zero?
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
s = Redland.librdf_model_size(@model.rdf_model)
|
49
|
+
if s < 0
|
50
|
+
raise RedlandError.new("Attempt to get size when using non-countable storage")
|
51
|
+
else
|
52
|
+
s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Find statements satisfying the given criteria.
|
57
|
+
# Scope can be:
|
58
|
+
# :all
|
59
|
+
# :first
|
60
|
+
def find(scope, options = {})
|
61
|
+
statement = Statement.new(options)
|
62
|
+
rdf_stream = Redland.librdf_model_find_statements(@model.rdf_model, statement.rdf_statement)
|
63
|
+
proxy = self.class.new(@model, rdf_stream)
|
64
|
+
|
65
|
+
case scope
|
66
|
+
when :first
|
67
|
+
proxy.first
|
68
|
+
when :all
|
69
|
+
if block_given?
|
70
|
+
proxy.each
|
71
|
+
else
|
72
|
+
proxy
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise RedlandError.new("Invalid search scope '#{scope}' specified.")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def all(options = {})
|
80
|
+
find(:all, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
class Node
|
4
|
+
|
5
|
+
attr_reader :rdf_node
|
6
|
+
|
7
|
+
# Create a RDF node.
|
8
|
+
# Argument can be:
|
9
|
+
# - an instance of URI - to create a RDF "resource",
|
10
|
+
# - an instance of Node - to create a copy of the node,
|
11
|
+
# - nil (or absent) - to create a "blank" node,
|
12
|
+
# - any other Ruby object, which can be coerced into a literal.
|
13
|
+
# If nothing else, a RedlandError is thrown.
|
14
|
+
#
|
15
|
+
# Note, that you cannot create a resource node from an URI string,
|
16
|
+
# it must be an instance of URI. Otherwise it is treated as a string literal.
|
17
|
+
def initialize(arg = nil)
|
18
|
+
@rdf_node = if arg.nil?
|
19
|
+
Redland.librdf_new_node_from_blank_identifier(Redlander.rdf_world, nil)
|
20
|
+
elsif arg.is_a?(URI)
|
21
|
+
Redland.librdf_new_node_from_uri_string(Redlander.rdf_world, arg.to_s)
|
22
|
+
elsif arg.is_a?(SWIG::TYPE_p_librdf_node_s)
|
23
|
+
# A special case, where you can pass an instance of SWIG::TYPE_p_librdf_node_s
|
24
|
+
# in order to create a Node from an internal RDF node representation.
|
25
|
+
arg
|
26
|
+
elsif arg.is_a?(Node)
|
27
|
+
Redland.librdf_new_node_from_node(arg.rdf_node)
|
28
|
+
else
|
29
|
+
value = arg.respond_to?(:xmlschema) ? arg.xmlschema : arg.to_s
|
30
|
+
datatype = Redlander.to_rdf_uri(XmlSchema.datatype_of(arg))
|
31
|
+
Redland.librdf_new_node_from_typed_literal(Redlander.rdf_world, value, nil, datatype)
|
32
|
+
end
|
33
|
+
|
34
|
+
raise RedlandError.new("Failed to create a new node") unless @rdf_node
|
35
|
+
ObjectSpace.define_finalizer(@rdf_node, proc { Redland.librdf_free_node(@rdf_node) })
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource?
|
39
|
+
!Redland.librdf_node_is_resource(@rdf_node).zero?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return true if node is a literal.
|
43
|
+
def literal?
|
44
|
+
!Redland.librdf_node_is_literal(@rdf_node).zero?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return true if node is a blank node.
|
48
|
+
def blank?
|
49
|
+
!Redland.librdf_node_is_blank(@rdf_node).zero?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return the datatype URI of the node.
|
53
|
+
# Returns nil if the node is not a literal, or has no datatype URI.
|
54
|
+
def datatype
|
55
|
+
if rdf_uri = Redland.librdf_node_get_literal_value_datatype_uri(@rdf_node)
|
56
|
+
ObjectSpace.define_finalizer(rdf_uri, proc { Redland.librdf_free_uri(rdf_uri) })
|
57
|
+
Redland.librdf_uri_to_string(rdf_uri)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Equivalency. Only works for comparing two Nodes.
|
62
|
+
def eql?(other_node)
|
63
|
+
!Redland.librdf_node_equals(@rdf_node, other_node.rdf_node).zero?
|
64
|
+
end
|
65
|
+
alias_method :==, :eql?
|
66
|
+
|
67
|
+
def hash
|
68
|
+
self.class.hash + to_s.hash
|
69
|
+
end
|
70
|
+
|
71
|
+
# Convert this node to a string (with a datatype suffix).
|
72
|
+
def to_s
|
73
|
+
Redland.librdf_node_to_string(@rdf_node)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Value of the literal node as a Ruby object instance.
|
77
|
+
def value
|
78
|
+
if resource?
|
79
|
+
if rdf_uri = Redland.librdf_node_get_uri(@rdf_node)
|
80
|
+
ObjectSpace.define_finalizer(rdf_uri, proc { Redland.librdf_free_uri(rdf_uri) })
|
81
|
+
URI.parse(Redland.librdf_uri_to_string(rdf_uri))
|
82
|
+
end
|
83
|
+
else
|
84
|
+
XmlSchema.instantiate(to_s)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'redlander/parser_proxy'
|
2
|
+
|
3
|
+
module Redlander
|
4
|
+
|
5
|
+
class Parser
|
6
|
+
|
7
|
+
attr_reader :rdf_parser
|
8
|
+
|
9
|
+
# Create a new parser.
|
10
|
+
# Name can be either of [:rdfxml, :ntriples, :turtle],
|
11
|
+
# or nil, which defaults to :rdfxml.
|
12
|
+
#
|
13
|
+
# TODO: Only a small subset of parsers is implemented,
|
14
|
+
# because the rest seem to be very buggy.
|
15
|
+
def initialize(name = :rdfxml)
|
16
|
+
# name, mime-type and syntax uri can all be nil, which defaults to :rdfxml parser
|
17
|
+
@rdf_parser = Redland.librdf_new_parser(Redlander.rdf_world, name.to_s, nil, nil)
|
18
|
+
raise RedlandError.new("Failed to create a new parser") unless @rdf_parser
|
19
|
+
ObjectSpace.define_finalizer(@rdf_parser, proc { Redland.librdf_free_parser(@rdf_parser) })
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse the content (String) into the model.
|
23
|
+
#
|
24
|
+
# Options are:
|
25
|
+
# :base_uri base URI (String or URI)
|
26
|
+
#
|
27
|
+
# Returns true on success, or false.
|
28
|
+
def from_string(model, content, options = {})
|
29
|
+
# FIXME: A bug (?) in Redland breaks NTriples parser if its input is not terminated with "\n"
|
30
|
+
content.concat("\n") unless content.end_with?("\n")
|
31
|
+
Redland.librdf_parser_parse_string_into_model(@rdf_parser, content, Redlander.to_rdf_uri(options[:base_uri]), model.rdf_model).zero?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parse the content from URI into the model.
|
35
|
+
# (It is possible to use "file:" schema for local files).
|
36
|
+
#
|
37
|
+
# Options are:
|
38
|
+
# :base_uri base URI (String or URI)
|
39
|
+
#
|
40
|
+
# Returns true on success, or false.
|
41
|
+
def from_uri(model, uri, options = {})
|
42
|
+
uri = URI.parse(uri)
|
43
|
+
uri = URI.parse("file://#{File.expand_path(uri.to_s)}") if uri.scheme.nil?
|
44
|
+
Redland.librdf_parser_parse_into_model(@rdf_parser, Redlander.to_rdf_uri(uri), Redlander.to_rdf_uri(options[:base_uri]), model.rdf_model).zero?
|
45
|
+
end
|
46
|
+
alias_method :from_file, :from_uri
|
47
|
+
|
48
|
+
def statements(content, options = {})
|
49
|
+
# BUG: Parser accumulates data from consequent runs??? WTF, Redland?!
|
50
|
+
# When parsing a series of files, parser reported a duplicate entry,
|
51
|
+
# then seemed to have stopped yielding statements at all.
|
52
|
+
|
53
|
+
# FIXME: A bug (?) in Redland breaks NTriples parser if its input is not terminated with "\n"
|
54
|
+
content.concat("\n") unless content.end_with?("\n")
|
55
|
+
ParserProxy.new(self, content, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
module ParsingInstanceMethods
|
62
|
+
|
63
|
+
def from_rdfxml(content, options = {})
|
64
|
+
parser = Parser.new(:rdfxml)
|
65
|
+
parser.from_string(self, content, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def from_ntriples(content, options = {})
|
69
|
+
parser = Parser.new(:ntriples)
|
70
|
+
parser.from_string(self, content, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def from_turtle(content, options = {})
|
74
|
+
parser = Parser.new(:turtle)
|
75
|
+
parser.from_string(self, content, options)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Load the model from an URI content.
|
79
|
+
#
|
80
|
+
# Options are:
|
81
|
+
# :format - content format [:rdfxml (default), :ntriples, :turtle]
|
82
|
+
# :base_uri - base URI
|
83
|
+
def from_uri(uri, options = {})
|
84
|
+
parser_options = options.dup
|
85
|
+
parser = Parser.new(parser_options.delete(:format) || :rdfxml)
|
86
|
+
parser.from_uri(self, uri, parser_options)
|
87
|
+
end
|
88
|
+
alias_method :from_file, :from_uri
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'redlander/statement'
|
2
|
+
|
3
|
+
module Redlander
|
4
|
+
|
5
|
+
class ParserProxy
|
6
|
+
|
7
|
+
include StatementIterator
|
8
|
+
|
9
|
+
def initialize(parser, content, options = {})
|
10
|
+
@model = nil # the yielded statements will not be bound to a model
|
11
|
+
@rdf_stream = Redland.librdf_parser_parse_string_as_stream(parser.rdf_parser, content, Redlander.to_rdf_uri(options[:base_uri]))
|
12
|
+
raise RedlandError.new("Failed to create a new stream") unless @rdf_stream
|
13
|
+
ObjectSpace.define_finalizer(@rdf_stream, proc { Redland.librdf_free_stream(@rdf_stream) })
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
class Serializer
|
4
|
+
|
5
|
+
# Create a new serializer.
|
6
|
+
# Name can be either of [:rdfxml, :ntriples, :turtle, :json, :dot],
|
7
|
+
# or nil, which defaults to :rdfxml.
|
8
|
+
#
|
9
|
+
# TODO: Only a small subset of parsers is implemented,
|
10
|
+
# because the rest seem to be very buggy.
|
11
|
+
def initialize(name = :rdfxml)
|
12
|
+
@rdf_serializer = Redland.librdf_new_serializer(Redlander.rdf_world, name.to_s, nil, nil)
|
13
|
+
raise RedlandError.new("Failed to create a new serializer") unless @rdf_serializer
|
14
|
+
ObjectSpace.define_finalizer(@rdf_serializer, proc { Redland.librdf_free_serializer(@rdf_serializer) })
|
15
|
+
end
|
16
|
+
|
17
|
+
# Serialize a model into a string.
|
18
|
+
#
|
19
|
+
# Options are:
|
20
|
+
# :base_uri base URI (String or URI)
|
21
|
+
def to_string(model, options = {})
|
22
|
+
Redland.librdf_serializer_serialize_model_to_string(@rdf_serializer, Redlander.to_rdf_uri(options[:base_uri]), model.rdf_model)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Serializes a model and stores it in a file
|
26
|
+
# filename - the name of the file to serialize to
|
27
|
+
# model - model instance
|
28
|
+
#
|
29
|
+
# Options are:
|
30
|
+
# :base_uri base URI (String or URI)
|
31
|
+
#
|
32
|
+
# Returns true on success, or false.
|
33
|
+
def to_file(model, filename, options = {})
|
34
|
+
Redland.librdf_serializer_serialize_model_to_file(@rdf_serializer, filename, Redlander.to_rdf_uri(options[:base_uri]), model.rdf_model).zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
module SerializingInstanceMethods
|
41
|
+
|
42
|
+
def to_rdfxml(options = {})
|
43
|
+
serializer = Serializer.new(:rdfxml)
|
44
|
+
serializer.to_string(self, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_ntriples(options = {})
|
48
|
+
serializer = Serializer.new(:ntriples)
|
49
|
+
serializer.to_string(self, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_turtle(options = {})
|
53
|
+
serializer = Serializer.new(:turtle)
|
54
|
+
serializer.to_string(self, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_json(options = {})
|
58
|
+
serializer = Serializer.new(:json)
|
59
|
+
serializer.to_string(self, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_dot(options = {})
|
63
|
+
serializer = Serializer.new(:dot)
|
64
|
+
serializer.to_string(self, options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Serialize the model into a file.
|
68
|
+
#
|
69
|
+
# Options are:
|
70
|
+
# :format - output format [:rdfxml (default), :ntriples, :turtle, :json, :dot]
|
71
|
+
# :base_uri - base URI
|
72
|
+
def to_file(filename, options = {})
|
73
|
+
serializer_options = options.dup
|
74
|
+
serializer = Serializer.new(serializer_options.delete(:format) || :rdfxml)
|
75
|
+
serializer.to_file(self, filename, serializer_options)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|