active-orient 0.42 → 0.79
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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/Gemfile +13 -5
- data/Guardfile +12 -4
- data/README.md +67 -280
- data/VERSION +1 -1
- data/active-orient.gemspec +6 -5
- data/bin/active-orient-0.6.gem +0 -0
- data/bin/active-orient-console +85 -0
- data/config/boot.rb +72 -1
- data/config/config.yml +10 -0
- data/config/connect.yml +9 -4
- data/examples/books.rb +92 -40
- data/examples/streets.rb +89 -85
- data/examples/test_commands.rb +97 -0
- data/examples/test_commands_2.rb +59 -0
- data/examples/test_commands_3.rb +55 -0
- data/examples/test_commands_4.rb +33 -0
- data/examples/time_graph.md +162 -0
- data/lib/active-orient.rb +75 -9
- data/lib/base.rb +238 -169
- data/lib/base_properties.rb +68 -60
- data/lib/class_utils.rb +226 -0
- data/lib/database_utils.rb +98 -0
- data/lib/init.rb +79 -0
- data/lib/java-api.rb +442 -0
- data/lib/jdbc.rb +211 -0
- data/lib/model/custom.rb +26 -0
- data/lib/model/edge.rb +70 -0
- data/lib/model/model.rb +134 -0
- data/lib/model/the_class.rb +607 -0
- data/lib/model/the_record.rb +266 -0
- data/lib/model/vertex.rb +236 -0
- data/lib/orientdb_private.rb +48 -0
- data/lib/other.rb +371 -0
- data/lib/railtie.rb +68 -0
- data/lib/rest/change.rb +147 -0
- data/lib/rest/create.rb +279 -0
- data/lib/rest/delete.rb +134 -0
- data/lib/rest/operations.rb +211 -0
- data/lib/rest/read.rb +171 -0
- data/lib/rest/rest.rb +112 -0
- data/lib/rest_disabled.rb +24 -0
- data/lib/support/logging.rb +38 -0
- data/lib/support/orient.rb +196 -0
- data/lib/support/orientquery.rb +469 -0
- data/rails.md +154 -0
- data/rails/activeorient.rb +32 -0
- data/rails/config.yml +10 -0
- data/rails/connect.yml +17 -0
- metadata +65 -24
- data/active-orient-0.4.gem +0 -0
- data/active-orient-0.41.gem +0 -0
- data/lib/model.rb +0 -468
- data/lib/orient.rb +0 -98
- data/lib/query.rb +0 -88
- data/lib/rest.rb +0 -1059
- data/lib/support.rb +0 -372
- data/test.rb +0 -4
- data/usecase.md +0 -91
@@ -0,0 +1,266 @@
|
|
1
|
+
module ModelRecord
|
2
|
+
############### RECORD FUNCTIONS ###############
|
3
|
+
|
4
|
+
############# GET #############
|
5
|
+
|
6
|
+
def from_orient # :nodoc:
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns just the name of the Class
|
11
|
+
|
12
|
+
def self.classname # :nodoc:
|
13
|
+
self.class.to_s.split(':')[-1]
|
14
|
+
end
|
15
|
+
=begin
|
16
|
+
flag whether a property exists on the Record-level
|
17
|
+
=end
|
18
|
+
def has_property? property
|
19
|
+
attributes.keys.include? property.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def properties
|
23
|
+
{ "@type" => "d", "@class" => self.metadata[:class] }.merge attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Obtain the RID of the Record (format: *00:00*)
|
28
|
+
#
|
29
|
+
|
30
|
+
def rid
|
31
|
+
begin
|
32
|
+
"#{@metadata[:cluster]}:#{@metadata[:record]}"
|
33
|
+
rescue
|
34
|
+
"0:0"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
=begin
|
38
|
+
The extended representation of RID (format: *#00:00* )
|
39
|
+
=end
|
40
|
+
def rrid
|
41
|
+
"#" + rid
|
42
|
+
end
|
43
|
+
alias to_orient rrid
|
44
|
+
|
45
|
+
def to_or
|
46
|
+
rid.rid? ? rrid : "{ #{embedded} }"
|
47
|
+
end
|
48
|
+
=begin
|
49
|
+
Query uses the current model-record as origin of the query.
|
50
|
+
|
51
|
+
It sends the OrientSupport::OrientQuery directly to the database and returns an
|
52
|
+
ActiveOrient::Model-Object or an Array of Model-Objects as result.
|
53
|
+
|
54
|
+
*Usage:* Query the Database by traversing through edges and vertices starting at a known location
|
55
|
+
|
56
|
+
=end
|
57
|
+
|
58
|
+
def query query
|
59
|
+
|
60
|
+
query.from = rrid if query.is_a? OrientSupport::OrientQuery
|
61
|
+
result = orientdb.execute do
|
62
|
+
query.to_s
|
63
|
+
end
|
64
|
+
if result.is_a? Array
|
65
|
+
OrientSupport::Array.new work_on: self, work_with: result
|
66
|
+
else
|
67
|
+
result
|
68
|
+
end # return value
|
69
|
+
end
|
70
|
+
|
71
|
+
=begin
|
72
|
+
Fires a »where-Query» to the database starting with the current model-record.
|
73
|
+
|
74
|
+
Attributes:
|
75
|
+
* a string ( obj.find "in().out().some_attribute >3" )
|
76
|
+
* a hash ( obj.find 'some_embedded_obj.name' => 'test' )
|
77
|
+
* an array
|
78
|
+
|
79
|
+
Returns the result-set, ie. a Query-Object which contains links to the addressed records.
|
80
|
+
|
81
|
+
=end
|
82
|
+
def find attributes = {}
|
83
|
+
q = OrientSupport::OrientQuery.new from: self, where: attributes
|
84
|
+
query q
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the version of the object
|
88
|
+
def version # :nodoc:
|
89
|
+
if document.present?
|
90
|
+
document.version
|
91
|
+
else
|
92
|
+
@metadata[:version]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def version= version # :nodoc:
|
97
|
+
@metadata[:version] = version
|
98
|
+
end
|
99
|
+
|
100
|
+
def increment_version # :nodoc:
|
101
|
+
@metadata[:version] += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
############# DELETE ###########
|
106
|
+
|
107
|
+
# Removes the Model-Instance from the database.
|
108
|
+
#
|
109
|
+
# It is overloaded in Vertex and Edge.
|
110
|
+
|
111
|
+
def remove
|
112
|
+
orientdb.delete_record self
|
113
|
+
ActiveOrient::Base.remove_rid self ##if is_edge? # removes the obj from the rid_store
|
114
|
+
end
|
115
|
+
|
116
|
+
alias delete remove
|
117
|
+
|
118
|
+
########### UPDATE ############
|
119
|
+
|
120
|
+
=begin
|
121
|
+
Convient update of the dataset by calling sql-patch
|
122
|
+
|
123
|
+
Previously changed attributes are saved to the database.
|
124
|
+
|
125
|
+
Using the optional :set argument ad-hoc attributes can be defined
|
126
|
+
|
127
|
+
obj = ActiveOrient::Model::Contracts.first
|
128
|
+
obj.name = 'new_name'
|
129
|
+
obj.update set: { yesterdays_event: 35 }
|
130
|
+
updates both, the »name« and the »yesterdays_event«-properties
|
131
|
+
|
132
|
+
_note:_ The keyword »set« is optional, thus
|
133
|
+
obj.update yesterdays_event: 35
|
134
|
+
is identical
|
135
|
+
=end
|
136
|
+
|
137
|
+
def update set:{}, add: nil, to: nil, **args
|
138
|
+
logger.progname = 'ActiveOrient::Model#Update'
|
139
|
+
|
140
|
+
if block_given? # calling vs. a block is used internally
|
141
|
+
# to remove an Item from lists and sets call update(remove: true){ query }
|
142
|
+
set_or_remove = args[:remove].present? ? "remove" : "set"
|
143
|
+
transfer_content from: query( "update #{rrid} #{set_or_remove} #{ yield } return after @this" )&.first
|
144
|
+
else
|
145
|
+
set.merge! args
|
146
|
+
# set.merge updated_at: DateTime.now
|
147
|
+
|
148
|
+
if rid.rid?
|
149
|
+
transfer_content from: db.update( self, set, version )
|
150
|
+
# if the updated dataset changed, drop the changes made siently
|
151
|
+
# self # return value
|
152
|
+
else # new record
|
153
|
+
@attributes.merge! set
|
154
|
+
save
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
# mocking active record
|
161
|
+
def update_attribute the_attribute, the_value # :nodoc:
|
162
|
+
update { " #{the_attribute} = #{the_value.to_or} " }
|
163
|
+
end
|
164
|
+
|
165
|
+
def update_attributes **args # :nodoc:
|
166
|
+
update args
|
167
|
+
end
|
168
|
+
|
169
|
+
########## SAVE ############
|
170
|
+
|
171
|
+
=begin
|
172
|
+
Saves the record by calling update or creating the record
|
173
|
+
|
174
|
+
ORD.create_class :a
|
175
|
+
a = A.new
|
176
|
+
a.test = 'test'
|
177
|
+
a.save
|
178
|
+
|
179
|
+
a = A.first
|
180
|
+
a.test = 'test'
|
181
|
+
a.save
|
182
|
+
|
183
|
+
=end
|
184
|
+
def save
|
185
|
+
transfer_content from: if rid.rid?
|
186
|
+
db.update self, attributes, version
|
187
|
+
else
|
188
|
+
db.create_record self, attributes: attributes, cache: false
|
189
|
+
end
|
190
|
+
ActiveOrient::Base.store_rid self
|
191
|
+
end
|
192
|
+
|
193
|
+
def reload!
|
194
|
+
transfer_content from: db.get_record(rid)
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
def transfer_content from:
|
200
|
+
# »from« can be either
|
201
|
+
# a model record (in case of create-record, get_record) or
|
202
|
+
# a hash containing {"@type"=>"d", "@rid"=>"#xx:yy", "@version"=>n, "@class"=>'a_classname'}
|
203
|
+
# and a list of updated properties (in case of db.update). Then update the version field and the
|
204
|
+
# attributes.
|
205
|
+
if from.is_a? ActiveOrient::Model
|
206
|
+
@metadata = from.metadata
|
207
|
+
@attributes = from.attributes
|
208
|
+
else
|
209
|
+
self.version = from['@version']
|
210
|
+
# throw from["@..."] away and convert keys to symbols, merge that into attributes
|
211
|
+
@attributes.merge! Hash[ from.delete_if{|k,_| k =~ /^@/}.map{|k,v| [k.to_sym, v.from_orient]}]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
########## CHECK PROPERTY ########
|
215
|
+
|
216
|
+
=begin
|
217
|
+
An Edge is defined
|
218
|
+
* when inherented from the superclass »E» (formal definition)
|
219
|
+
* if it has an in- and an out property
|
220
|
+
|
221
|
+
Actually we just check the second term as we trust the constuctor to work properly
|
222
|
+
=end
|
223
|
+
|
224
|
+
def is_edge? # :nodoc:
|
225
|
+
attributes.keys.include?('in') && attributes.keys.include?('out')
|
226
|
+
end
|
227
|
+
|
228
|
+
=begin
|
229
|
+
How to handle other calls
|
230
|
+
|
231
|
+
* if attribute is specified, display it
|
232
|
+
* if attribute= is provided, assign to the known property or create a new one
|
233
|
+
|
234
|
+
Example:
|
235
|
+
ORD.create_class :a
|
236
|
+
a = A.new
|
237
|
+
a.test= 'test' # <--- attribute: 'test=', argument: 'test'
|
238
|
+
a.test # <--- attribute: 'test' --> fetch attributes[:test]
|
239
|
+
|
240
|
+
Assignments are performed only in ruby-space.
|
241
|
+
Automatic database-updates are deactivated for now
|
242
|
+
=end
|
243
|
+
def method_missing *args
|
244
|
+
# if the first entry of the parameter-array is a known attribute
|
245
|
+
# proceed with the assignment
|
246
|
+
if args.size == 1
|
247
|
+
attributes[args.first.to_sym] # return the attribute-value
|
248
|
+
elsif args[0][-1] == "="
|
249
|
+
if args.size == 2
|
250
|
+
# if rid.rid?
|
251
|
+
# update set:{ args[0][0..-2] => args.last }
|
252
|
+
# else
|
253
|
+
self.attributes[ args[0][0..-2] ] = args.last
|
254
|
+
# end
|
255
|
+
else
|
256
|
+
self.attributes[ args[0][0..-2] ] = args[1 .. -1]
|
257
|
+
# update set: {args[0][0..-2] => args[1 .. -1] } if rid.rid?
|
258
|
+
end
|
259
|
+
else
|
260
|
+
raise NameError, "Unknown method call #{args.first.to_s}", caller
|
261
|
+
end
|
262
|
+
end
|
263
|
+
#end
|
264
|
+
|
265
|
+
|
266
|
+
end
|
data/lib/model/vertex.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
class V < ActiveOrient::Model
|
2
|
+
## link to the library-class
|
3
|
+
|
4
|
+
=begin
|
5
|
+
specialized creation of vertices, overloads model#create
|
6
|
+
=end
|
7
|
+
def self.create( **keyword_arguments )
|
8
|
+
new_vert = db.create_vertex self, attributes: keyword_arguments
|
9
|
+
new_vert = new_vert.pop if new_vert.is_a?( Array) && new_vert.size == 1
|
10
|
+
if new_vert.nil?
|
11
|
+
logger.error('Vertex'){ "Table #{ref_name} ->> create failed: #{keyword_arguments.inspect}" }
|
12
|
+
elsif block_given?
|
13
|
+
yield new_vert
|
14
|
+
else
|
15
|
+
new_vert # returns the created vertex (or an array of created vertices)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
=begin
|
19
|
+
Vertex#delete fires a "delete vertex" command to the database.
|
20
|
+
The where statement can be empty ( "" or {}"), then all vertices are removed
|
21
|
+
|
22
|
+
The rid-cache is reseted, too
|
23
|
+
=end
|
24
|
+
def self.delete where: ""
|
25
|
+
db.execute { "delete vertex #{ref_name} #{db.compose_where(where)}" }
|
26
|
+
reset_rid_store
|
27
|
+
end
|
28
|
+
|
29
|
+
#Present Classes (Hierarchy)
|
30
|
+
#---
|
31
|
+
#- - E
|
32
|
+
# - - - e1
|
33
|
+
# - - e2
|
34
|
+
# - e3
|
35
|
+
#- - V
|
36
|
+
# - - - v1
|
37
|
+
# - - v2
|
38
|
+
|
39
|
+
#v.to_human
|
40
|
+
# => "<V2[#36:0]: in: {E2=>1}, node : 4>"
|
41
|
+
#
|
42
|
+
# v.detect_edges( :in, 2).to_human
|
43
|
+
# => ["<E2: in : #<V2:0x0000000002e66228>, out : #<V1:0x0000000002ed0060>>"]
|
44
|
+
# v.detect_edges( :in, E1).to_human
|
45
|
+
# => ["<E2: in : #<V2:0x0000000002e66228>, out : #<V1:0x0000000002ed0060>>"]
|
46
|
+
# v.detect_edges( :in, /e/).to_human
|
47
|
+
# => ["<E2: in : #<V2:0x0000000002e66228>, out : #<V1:0x0000000002ed0060>>"]
|
48
|
+
#
|
49
|
+
def detect_edges kind = :in, edge_name = nil # :nodoc:
|
50
|
+
## returns a list of inherented classes
|
51
|
+
get_superclass = ->(e) do
|
52
|
+
n = orientdb.get_db_superclass(e)
|
53
|
+
n =='E' ? e : e + ',' + get_superclass[n]
|
54
|
+
end
|
55
|
+
if edge_name.nil?
|
56
|
+
edges(kind).map &:expand
|
57
|
+
else
|
58
|
+
e_name = if edge_name.is_a?(Regexp)
|
59
|
+
edge_name
|
60
|
+
else
|
61
|
+
Regexp.new case edge_name
|
62
|
+
when Class
|
63
|
+
edge_name.ref_name
|
64
|
+
when String
|
65
|
+
edge_name
|
66
|
+
when Symbol
|
67
|
+
edge_name.to_s
|
68
|
+
when Numeric
|
69
|
+
edge_name.to_i.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
the_edges = @metadata[:edges][kind].find_all{|y| get_superclass[y].split(',').detect{|x| x =~ e_name } }
|
73
|
+
|
74
|
+
the_edges.map do | the_edge|
|
75
|
+
candidate= attributes["#{kind.to_s}_#{the_edge}".to_sym]
|
76
|
+
candidate.present? ? candidate.map( &:expand ).first : nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Lists all connected Vertices
|
82
|
+
#
|
83
|
+
# The Edge-classes can be specified via Classname or a regular expression.
|
84
|
+
#
|
85
|
+
# If a regular expression is used, the database-names are searched.
|
86
|
+
def nodes in_or_out = :out, via: nil, where: nil, expand: false
|
87
|
+
if via.present?
|
88
|
+
edges = detect_edges( in_or_out, via )
|
89
|
+
detected_nodes = edges.map do |e|
|
90
|
+
q = OrientSupport::OrientQuery.new
|
91
|
+
q.nodes in_or_out, via: e.class, where: where, expand: expand
|
92
|
+
query( q )
|
93
|
+
end.first
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
=begin
|
100
|
+
»in« and »out« provide the main access to edges.
|
101
|
+
»in» is a reserved keyword. Therfore its only an alias to `in_e`.
|
102
|
+
|
103
|
+
If called without a parameter, all connected edges are retrieved.
|
104
|
+
|
105
|
+
If called with a string, symbol or class, the edge-class is resolved and even inherented
|
106
|
+
edges are retrieved.
|
107
|
+
|
108
|
+
=end
|
109
|
+
|
110
|
+
def in_e edge_name= nil
|
111
|
+
detect_edges :in, edge_name
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :in, :in_e
|
115
|
+
|
116
|
+
def out edge_name = nil
|
117
|
+
detect_edges :out, edge_name
|
118
|
+
end
|
119
|
+
=begin
|
120
|
+
Retrieves connected edges
|
121
|
+
|
122
|
+
The basic usage is to fetch all/ incomming/ outgoing edges
|
123
|
+
|
124
|
+
Model-Instance.edges :in # :out | :all
|
125
|
+
|
126
|
+
One can filter specific edges by providing parts of the edge-name
|
127
|
+
|
128
|
+
Model-Instance.edges 'in_sector'
|
129
|
+
Model-Instance.edges /sector/
|
130
|
+
|
131
|
+
The method returns an array of rid's.
|
132
|
+
|
133
|
+
Example:
|
134
|
+
|
135
|
+
Industry.first.attributes.keys
|
136
|
+
=> ["in_sector_classification", "k", "name", "created_at", "updated_at"] # edge--> in ...
|
137
|
+
|
138
|
+
Industry.first.edges :out
|
139
|
+
=> []
|
140
|
+
|
141
|
+
Industry.first.edges :in
|
142
|
+
=> ["#61:0", "#61:9", "#61:21", "#61:33", "#61:39", "#61:93", "#61:120", "#61:150", "#61:240", "#61:252", "#61:264", "#61:279", "#61:303", "#61:339" ...]
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
To fetch the associated records use the ActiveOrient::Model.autoload_object method
|
147
|
+
|
148
|
+
ActiveOrient::Model.autoload_object Industry.first.edges( :in).first
|
149
|
+
# or
|
150
|
+
Industry.autoload_object Industry.first.edges( /sector/ ).first
|
151
|
+
=> #<SectorClassification:0x00000002daad20 @metadata={"type"=>"d", "class"=>"sector_classification", "version"=>1, "fieldTypes"=>"out=x,in=x", "cluster"=>61, "record"=>0},(...)
|
152
|
+
|
153
|
+
=end
|
154
|
+
|
155
|
+
def edges kind=:all # :all, :in, :out
|
156
|
+
expression = case kind
|
157
|
+
when :all
|
158
|
+
/^in|^out/
|
159
|
+
when :in
|
160
|
+
/^in/
|
161
|
+
when :out
|
162
|
+
/^out/
|
163
|
+
when String
|
164
|
+
/#{kind}/
|
165
|
+
when Regexp
|
166
|
+
kind
|
167
|
+
else
|
168
|
+
return []
|
169
|
+
end
|
170
|
+
|
171
|
+
edges = attributes.keys.find_all{ |x| x =~ expression }
|
172
|
+
edges.map{|x| attributes[x]}.flatten
|
173
|
+
end
|
174
|
+
|
175
|
+
=begin
|
176
|
+
»in_edges« and »out_edges« are shortcuts to »edges :in« and »edges :out«
|
177
|
+
|
178
|
+
Its easy to expand the result:
|
179
|
+
tg.out( :ohlc).out.out_edges
|
180
|
+
=> [["#102:11032", "#121:0"]]
|
181
|
+
tg.out( :ohlc).out.out_edges.from_orient
|
182
|
+
=> [[#<TG::GRID_OF:0x00000002620e38
|
183
|
+
|
184
|
+
this displays the out-edges correctly
|
185
|
+
|
186
|
+
whereas
|
187
|
+
tg.out( :ohlc).out.edges( :out)
|
188
|
+
=> [["#101:11032", "#102:11032", "#94:10653", "#121:0"]]
|
189
|
+
|
190
|
+
returns all edges. The parameter (:out) is not recognized, because out is already a nested array.
|
191
|
+
|
192
|
+
this
|
193
|
+
tg.out( :ohlc).first.out.edges( :out)
|
194
|
+
is a walkaround, but using in_- and out_edges is more elegant.
|
195
|
+
=end
|
196
|
+
def in_edges
|
197
|
+
edges :in
|
198
|
+
end
|
199
|
+
def out_edges
|
200
|
+
edges :out
|
201
|
+
end
|
202
|
+
|
203
|
+
# def remove
|
204
|
+
# db.delete_vertex self
|
205
|
+
# end
|
206
|
+
=begin
|
207
|
+
Human readable represantation of Vertices
|
208
|
+
|
209
|
+
Format: < Classname : Edges, Attributes >
|
210
|
+
=end
|
211
|
+
def to_human
|
212
|
+
count_and_display_classes = ->(array){array.map(&:class)&.group_by(&:itself)&.transform_values(&:count)}
|
213
|
+
|
214
|
+
the_ins = count_and_display_classes[ in_e]
|
215
|
+
the_outs = count_and_display_classes[ out]
|
216
|
+
|
217
|
+
in_and_out = in_edges.empty? ? "" : "in: #{the_ins}, "
|
218
|
+
in_and_out += out_edges.empty? ? "" : "out: #{the_outs}, "
|
219
|
+
|
220
|
+
|
221
|
+
#Default presentation of ActiveOrient::Model-Objects
|
222
|
+
|
223
|
+
"<#{self.class.to_s.demodulize}[#{rid}]: " + in_and_out + content_attributes.map do |attr, value|
|
224
|
+
v= case value
|
225
|
+
when ActiveOrient::Model
|
226
|
+
"< #{self.class.to_s.demodulize} : #{value.rid} >"
|
227
|
+
when OrientSupport::Array
|
228
|
+
value.to_s
|
229
|
+
# value.rrid #.to_human #.map(&:to_human).join("::")
|
230
|
+
else
|
231
|
+
value.from_orient
|
232
|
+
end
|
233
|
+
"%s : %s" % [ attr, v] unless v.nil?
|
234
|
+
end.compact.sort.join(', ') + ">".gsub('"' , ' ')
|
235
|
+
end
|
236
|
+
end
|