triannon 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c2ebd733067b7971cd2548562f807c5feed3dc31
4
- data.tar.gz: 6809922b340468bc2201c67d7f4c45656a24ffad
3
+ metadata.gz: 4bc729d85bad88af4e64d97bb59bcb387cf24bcd
4
+ data.tar.gz: c64868417a72673f936d3ab09e465bd3027f5b28
5
5
  SHA512:
6
- metadata.gz: 2701115eac46a00f11d06484c403da5c30c5a4f05bea5188cc2a80b4cdec54e693b0f5bbb15b882f2327e3e1cc494f6a4fd0c3e6c2a842beb3ad48ad1c1a282b
7
- data.tar.gz: 808a6fa21bca5e4bf5a5426c3a7860af1a8134dfb58c7cddb76368351017e8b71a876007085805fecdbbf00a165a351cca508c982a96b038f7d99f4cd1553cee
6
+ metadata.gz: 1bc829a33741ab23ca40e69d981f1701b2ce0556618274f107727dee8403a7481d12cfe3eefb207d761d275ec7dcdbda9cc1f4cb0e488e67617154edd917e544
7
+ data.tar.gz: 2e8440b488dde0e2dfb9f826979da480552209614cd5150108fb6be57bf17b2f28ca525e387bd4178502e4cfc02e3fe1683bd1e47e07b332fbd3f2f2736ace9c
data/README.md CHANGED
@@ -44,6 +44,65 @@ Generate the root annotations container on the LDP server
44
44
  $ rake triannon:create_root_container
45
45
  ```
46
46
 
47
+ # Client Interactions with Triannon
48
+
49
+ ### Get a list of annos
50
+ NOTE: implementation of Annotation Lists is coming!
51
+ GET: http://(host)/
52
+ GET: http://(host)/annotations
53
+
54
+ ### Get a particular anno
55
+ GET: http://(host)/annotations/(anno_id)
56
+ * use HTTP Accept header with mime type to indicate desired format
57
+ ** default: jsonld
58
+ *** indicate desired context url in the HTTP Accept header thus:
59
+ **** Accept: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
60
+ **** Accept: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
61
+
62
+ ** also supports turtle, rdfxml, json, html
63
+ *** indicated desired context url for jsonld as json in the HTTP Link header thus:
64
+ **** Accept: application/json
65
+ **** Link: http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
66
+ ***** note that the "type" part is optional and refers to the type of the rel, which is the reference for all json-ld contexts.
67
+ ** see https://github.com/sul-dlss/triannon/blob/master/app/controllers/triannon/annotations_controller.rb #show method for mime formats accepted
68
+
69
+ #### JSON-LD context
70
+ You can request IIIF or OA context for jsonld.
71
+
72
+ The correct way:
73
+ GET: http://(host)/annotations/(anno_id)
74
+ * use HTTP Accept header with mime type and context url:
75
+ ** Accept: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
76
+ ** Accept: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
77
+
78
+ You can also use either of these methods (with the correct HTTP Accept header):
79
+
80
+ GET: http://(host)/annotations/iiif/(anno_id)
81
+ GET: http://(host)/annotations/(anno_id)?jsonld_context=iiif
82
+
83
+ GET: http://(host)/annotations/oa/(anno_id)
84
+ GET: http://(host)/annotations/(anno_id)?jsonld_context=oa
85
+
86
+ Note that OA (Open Annotation) is the default context if none is specified.
87
+
88
+ ### Create an anno
89
+ POST: http://(host)/annotations
90
+ * the body of the HTTP request should contain the annotation, as jsonld, turtle, or rdfxml
91
+ * the Content-Type header should be the mime type matching the body
92
+ * the anno to be created should NOT already have an assigned @id
93
+ * to get a particular format back, use the HTTP Accept header
94
+ ** to get a particular context for jsonld, do one of the following:
95
+ **** Accept: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
96
+ **** Accept: application/ld+json; profile="http://iiif.io/api/presentation/2/context.json"
97
+ ** to get a particular jsonld context for jsonld as json, specify it in the HTTP Link header thus:
98
+ **** Accept: application/json
99
+ **** Link: http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
100
+ ***** note that the "type" part is optional and refers to the type of the rel, which is the reference for all json-ld contexts.
101
+
102
+ ### Delete an anno
103
+ DELETE: http://(host)/annotations/(anno_id)
104
+
105
+
47
106
  ## Running the application in development
48
107
 
49
108
  There is a bundled rake task for running the test app, but there is some one-time set up.
@@ -57,8 +116,7 @@ $ rake jetty:unzip
57
116
  ```
58
117
  ##### Set up a Triannon flavored Solr
59
118
  ```console
60
- $ cp config/solr/solr.xml jetty/solr
61
- $ cp config/solr/triannon-core jetty/solr
119
+ rake triannon:solr_jetty_setup
62
120
  ```
63
121
 
64
122
  ##### Set up a runnable Rails app that uses triannon gem
data/Rakefile CHANGED
@@ -69,6 +69,12 @@ namespace :triannon do
69
69
  desc 'run test rails console w triannon but no jetty'
70
70
  task :console => :console_no_jetty
71
71
 
72
+
73
+ desc "set up triannon solr in test jetty"
74
+ task :solr_jetty_setup do
75
+ `cp -r config/solr/triannon-core jetty/solr`
76
+ `cp config/solr/solr.xml jetty/solr`
77
+ end
72
78
  end
73
79
 
74
80
 
@@ -3,7 +3,7 @@ require_dependency "triannon/application_controller"
3
3
  module Triannon
4
4
  class AnnotationsController < ApplicationController
5
5
  before_action :default_format_jsonld, only: [:show]
6
- before_action :set_annotation, only: [:show, :edit, :update, :destroy]
6
+ before_action :set_annotation, only: [:show, :update, :destroy]
7
7
  rescue_from Triannon::ExternalReferenceError, with: :ext_ref_error
8
8
 
9
9
  # GET /annotations
@@ -13,8 +13,16 @@ module Triannon
13
13
 
14
14
  # GET /annotations/1
15
15
  def show
16
+ # TODO: json.set! "@context", Triannon::JsonldContext::OA_DATED_CONTEXT_URL - would this work?
16
17
  respond_to do |format|
17
- format.jsonld { render_jsonld_per_context (params[:jsonld_context]) }
18
+ format.jsonld {
19
+ context_url = context_url_from_accept ? context_url_from_accept : context_url_from_link
20
+ if context_url && context_url == Triannon::JsonldContext::IIIF_CONTEXT_URL
21
+ render_jsonld_per_context("iiif", "application/ld+json")
22
+ else
23
+ render_jsonld_per_context(params[:jsonld_context], "application/ld+json")
24
+ end
25
+ }
18
26
  format.ttl {
19
27
  accept_return_type = mime_type_from_accept(["application/x-turtle", "text/turtle"])
20
28
  render :body => @annotation.graph.to_ttl, content_type: accept_return_type if accept_return_type }
@@ -23,7 +31,13 @@ module Triannon
23
31
  render :body => @annotation.graph.to_rdfxml, content_type: accept_return_type if accept_return_type }
24
32
  format.json {
25
33
  accept_return_type = mime_type_from_accept(["application/json", "text/x-json", "application/jsonrequest"])
26
- render_jsonld_per_context(params[:jsonld_context], accept_return_type) }
34
+ context_url = context_url_from_link ? context_url_from_link : context_url_from_accept
35
+ if context_url && context_url == Triannon::JsonldContext::IIIF_CONTEXT_URL
36
+ render_jsonld_per_context("iiif", accept_return_type)
37
+ else
38
+ render_jsonld_per_context(params[:jsonld_context], accept_return_type)
39
+ end
40
+ }
27
41
  format.xml {
28
42
  accept_return_type = mime_type_from_accept(["application/xml", "text/xml", "application/x-xml"])
29
43
  render :xml => @annotation.graph.to_rdfxml, content_type: accept_return_type if accept_return_type }
@@ -44,7 +58,9 @@ module Triannon
44
58
  # POST /annotations
45
59
  def create
46
60
  # FIXME: this is probably a bad way of allowing app form to be used as well as direct post requests
47
- if params["annotation"]
61
+ # see https://github.com/sul-dlss/triannon/issues/90 -- prob just want to fix the form to do a POST
62
+ # note that need to check for empty? if HTTP Header Content-Type is json (but not jsonld).
63
+ if params["annotation"] && !params["annotation"].empty?
48
64
  # it's from app html form
49
65
  params.require(:annotation).permit(:data)
50
66
  if params["annotation"]["data"]
@@ -52,13 +68,43 @@ module Triannon
52
68
  end
53
69
  else
54
70
  # it's a direct post request
55
- @annotation = Annotation.new(:data => request.body.read)
71
+ content_type = request.headers["Content-Type"]
72
+ @annotation = Annotation.new({:data => request.body.read, :expected_content_type => content_type})
56
73
  end
57
74
 
58
75
  if @annotation.save
59
- redirect_to @annotation, status: 201, notice: 'Annotation was successfully created.'
76
+ default_format_jsonld # NOTE: this must be here and not in before_filter or we get Missing template errors
77
+ respond_to do |format|
78
+ format.jsonld {
79
+ context_url = context_url_from_link ? context_url_from_link : context_url_from_accept
80
+ if context_url && context_url == Triannon::JsonldContext::IIIF_CONTEXT_URL
81
+ render :json => @annotation.jsonld_iiif, status: 201, content_type: "application/ld+json", notice: "Annotation #{@annotation.id} was successfully created."
82
+ else
83
+ render :json => @annotation.jsonld_oa, status: 201, content_type: "application/ld+json", notice: "Annotation #{@annotation.id} was successfully created."
84
+ end
85
+ }
86
+ format.ttl {
87
+ accept_return_type = mime_type_from_accept(["application/x-turtle", "text/turtle"])
88
+ render :body => @annotation.graph.to_ttl, status: 201, notice: "Annotation #{@annotation.id} was successfully created.", content_type: accept_return_type if accept_return_type }
89
+ format.rdfxml {
90
+ accept_return_type = mime_type_from_accept(["application/rdf+xml", "text/rdf+xml", "text/rdf"])
91
+ render :body => @annotation.graph.to_rdfxml, status: 201, notice: "Annotation #{@annotation.id} was successfully created.", content_type: accept_return_type if accept_return_type }
92
+ format.json {
93
+ accept_return_type = mime_type_from_accept(["application/json", "text/x-json", "application/jsonrequest"])
94
+ context_url = context_url_from_link ? context_url_from_link : context_url_from_accept
95
+ if context_url && context_url == Triannon::JsonldContext::IIIF_CONTEXT_URL
96
+ render :json => @annotation.jsonld_iiif, status: 201, notice: "Annotation #{@annotation.id} was successfully created.", content_type: accept_return_type if accept_return_type
97
+ else
98
+ render :json => @annotation.jsonld_oa, status: 201, notice: "Annotation #{@annotation.id} was successfully created.", content_type: accept_return_type if accept_return_type
99
+ end
100
+ }
101
+ format.xml {
102
+ accept_return_type = mime_type_from_accept(["application/xml", "text/xml", "application/x-xml"])
103
+ render :body => @annotation.graph.to_rdfxml, status: 201, notice: "Annotation #{@annotation.id} was successfully created.", content_type: accept_return_type if accept_return_type }
104
+ format.html { render :show, location: @annotation, status: 201, content_type: "text/html", notice: "Annotation #{@annotation.id} was successfully created." }
105
+ end
60
106
  else
61
- render :new
107
+ render :new, status: 400
62
108
  end
63
109
  end
64
110
 
@@ -77,62 +123,108 @@ module Triannon
77
123
  @annotation.destroy
78
124
  redirect_to annotations_url, status: 204, notice: 'Annotation was successfully destroyed.'
79
125
  end
126
+
127
+ private
80
128
 
81
- private
82
- # Use callbacks to share common setup or constraints between actions.
83
- def set_annotation
84
- @annotation = Annotation.find(params[:id])
129
+ def set_annotation
130
+ @annotation = Annotation.find(params[:id])
131
+ end
132
+
133
+ # set format to jsonld if it isn't already set
134
+ def default_format_jsonld
135
+ if ((!request.accept || request.accept.empty?) && (!params[:format] || params[:format].empty?))
136
+ request.format = "jsonld"
85
137
  end
86
-
87
- def default_format_jsonld
88
- if ((!request.accept || request.accept.empty?) && (!params[:format] || params[:format].empty?))
89
- request.format = "jsonld"
138
+ end
139
+
140
+ # find first mime type from request.accept that matches return mime type
141
+ def mime_type_from_accept(return_mime_types)
142
+ @mime_type_from_accept ||= begin
143
+ if request.accept && request.accept.is_a?(String)
144
+ accept_mime_types = request.accept.split(',')
145
+ accept_mime_types.each { |mime_type|
146
+ mime_str = mime_type.split("; profile=").first.strip
147
+ if return_mime_types.include? mime_str
148
+ return mime_str
149
+ end
150
+ }
90
151
  end
91
152
  end
153
+ end
92
154
 
93
- # find first mime type from request.accept that matches return mime type
94
- def mime_type_from_accept(return_mime_types)
95
- @mime_type_from_accept ||= begin
96
- if request.accept && request.accept.is_a?(String)
97
- accepted_formats = request.accept.split(',')
98
- accepted_formats.each { |accepted_format|
99
- if return_mime_types.include? accepted_format
100
- return accepted_format
101
- end
102
- }
155
+ # parse the Accept HTTP header for the value of profile if it is a request for jsonld or json
156
+ # e.g. Accept: application/ld+json; profile="http://www.w3.org/ns/oa-context-20130208.json"
157
+ # @return [String] url for jsonld @context or nil if missing or non-jsonld/json format
158
+ def context_url_from_accept
159
+ if request.format == "jsonld" || request.format == "json"
160
+ accept_str = request.accept
161
+ if accept_str && accept_str.split("profile=") && accept_str.split("profile=").last
162
+ context_url = accept_str.split("profile=").last.strip
163
+ context_url = context_url[1, context_url.size] if context_url.start_with?('"')
164
+ context_url = context_url[0, context_url.size-1] if context_url.end_with?('"')
165
+ case context_url
166
+ when Triannon::JsonldContext::OA_DATED_CONTEXT_URL,
167
+ Triannon::JsonldContext::OA_CONTEXT_URL,
168
+ Triannon::JsonldContext::IIIF_CONTEXT_URL
169
+ context_url
170
+ else
171
+ nil
103
172
  end
104
173
  end
105
174
  end
175
+ end
106
176
 
107
- # handle Triannon::ExternalReferenceError
108
- def ext_ref_error(exception)
109
- render plain: exception.message, status: 403
110
- end
111
-
112
- # render json_ld respecting requested context
113
- # @param [String] req_context set to "iiif" or "oa". Default is OA
114
- # @param [String] mime_type the mime type to be set in the Content-Type header of the HTTP response
115
- def render_jsonld_per_context (req_context, mime_type=nil)
116
- case req_context
117
- when "iiif", "IIIF"
118
- if mime_type
119
- render :json => @annotation.jsonld_iiif, content_type: mime_type
177
+ # parse the Accept HTTP Link for the value of rel if it is a request for jsonld or json
178
+ # e.g. Link: http://www.w3.org/ns/oa.json; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
179
+ # note that the "type" part is optional
180
+ # @return [String] url for jsonld @context or nil if missing or non-jsonld/json format
181
+ def context_url_from_link
182
+ if request.format == "jsonld" || request.format == "json"
183
+ link_str = request.headers["Link"]
184
+ if link_str && link_str.split("; rel=") && link_str.split("; rel=").first
185
+ context_url = link_str.split("; rel=").first.strip
186
+ case context_url
187
+ when Triannon::JsonldContext::OA_DATED_CONTEXT_URL,
188
+ Triannon::JsonldContext::OA_CONTEXT_URL,
189
+ Triannon::JsonldContext::IIIF_CONTEXT_URL
190
+ context_url
120
191
  else
121
- render :json => @annotation.jsonld_iiif
122
- end
123
- when "oa", "OA"
124
- if mime_type
125
- render :json => @annotation.jsonld_oa, content_type: mime_type
126
- else
127
- render :json => @annotation.jsonld_oa
128
- end
192
+ nil
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # handle Triannon::ExternalReferenceError
199
+ def ext_ref_error(exception)
200
+ render plain: exception.message, status: 403
201
+ end
202
+
203
+ # render json_ld respecting requested context
204
+ # @param [String] req_context set to "iiif" or "oa". Default is oa
205
+ # @param [String] mime_type the mime type to be set in the Content-Type header of the HTTP response
206
+ def render_jsonld_per_context (req_context, mime_type=nil)
207
+ case req_context
208
+ when "iiif", "IIIF"
209
+ if mime_type
210
+ render :json => @annotation.jsonld_iiif, content_type: mime_type
129
211
  else
130
- if mime_type
131
- render :json => @annotation.jsonld_oa, content_type: mime_type
132
- else
133
- render :json => @annotation.jsonld_oa
134
- end
135
- end
212
+ render :json => @annotation.jsonld_iiif
213
+ end
214
+ when "oa", "OA"
215
+ if mime_type
216
+ render :json => @annotation.jsonld_oa, content_type: mime_type
217
+ else
218
+ render :json => @annotation.jsonld_oa
219
+ end
220
+ else
221
+ if mime_type
222
+ render :json => @annotation.jsonld_oa, content_type: mime_type
223
+ else
224
+ render :json => @annotation.jsonld_oa
225
+ end
136
226
  end
137
- end
138
- end
227
+ end
228
+
229
+ end # class AnnotationsController
230
+ end # module Triannon
@@ -6,7 +6,7 @@ module Triannon
6
6
  after_save :solr_save
7
7
  after_destroy :solr_delete
8
8
 
9
- attr_accessor :id, :data
9
+ attr_accessor :id, :data, :expected_content_type
10
10
 
11
11
  validates_each :data do |record, attr, value|
12
12
  record.errors.add attr, 'less than 30 chars' if value.to_s.length < 30
@@ -45,7 +45,7 @@ module Triannon
45
45
  _run_save_callbacks do
46
46
  # check if valid?
47
47
  graph
48
- @id = Triannon::LdpWriter.create_anno self
48
+ @id = Triannon::LdpWriter.create_anno self if graph && graph.size > 2
49
49
  end
50
50
  end
51
51
 
@@ -113,28 +113,69 @@ protected
113
113
 
114
114
  private
115
115
 
116
- # loads RDF::Graph from data attribute. If data is in json-ld, converts it to turtle.
116
+ # loads RDF::Graph from data attribute. If data is in json-ld or rdfxml, converts it to turtle.
117
117
  def data_to_graph
118
118
  if data
119
119
  data.strip!
120
- case data
121
- when /\A\{.+\}\Z/m # (Note: \A and \Z and m are needed instead of ^$ due to \n in data)
122
- g ||= RDF::Graph.new << JSON::LD::API.toRdf(json_ld) if json_ld
123
- self.data = g.dump(:ttl) if g
124
- when /\A<.+>\Z/m # (Note: \A and \Z and m are needed instead of ^$ due to \n in data)
125
- g = RDF::Graph.new
126
- g.from_rdfxml(data)
127
- g = nil if g.size == 0
128
- when /\.\Z/ # (Note: \Z is needed instead of $ due to \n in data)
129
- # turtle ends in period
130
- g = RDF::Graph.new
131
- g.from_ttl(data)
132
- g = nil if g.size == 0
120
+ if expected_content_type
121
+ case Mime::Type.lookup(expected_content_type).symbol
122
+ when :jsonld, :json
123
+ g = jsonld_to_graph
124
+ when :ttl
125
+ g = ttl_to_graph
126
+ when :rdfxml, :xml
127
+ g = rdfxml_to_graph
128
+ else
129
+ g = nil
130
+ end
131
+ else # infer the content type from the content itself
132
+ case data
133
+ # \A and \Z and m are needed instead of ^$ due to \n in data
134
+ when /\A\{.+\}\Z/m
135
+ g = jsonld_to_graph
136
+ when /\A<.+>\Z/m
137
+ g = rdfxml_to_graph
138
+ when /\.\Z/ # turtle ends in period
139
+ g = ttl_to_graph
140
+ else
141
+ g = nil
142
+ end
133
143
  end
134
144
  end
135
145
  g
136
146
  end
137
147
 
148
+ # create and load an RDF::Graph object from turtle in data attrib
149
+ # @return [RDF::Graph] populated RDF::Graph object, or nil
150
+ def ttl_to_graph
151
+ g = RDF::Graph.new.from_ttl(data)
152
+ g = nil if g && g.size == 0
153
+ g
154
+ end
155
+
156
+ # create and load an RDF::Graph object from jsonld in data attrib
157
+ # SIDE EFFECT: converts data to turtle for LdpWriter
158
+ # @return [RDF::Graph] populated RDF::Graph object, or nil
159
+ def jsonld_to_graph
160
+ # need to do this to avoid external lookup of jsonld context
161
+ g ||= RDF::Graph.new << JSON::LD::API.toRdf(json_ld) if json_ld
162
+ g = nil if g && g.size == 0
163
+ self.data = g.dump(:ttl) if g # LdpWriter expects ttl
164
+ g
165
+ end
166
+
167
+ # create and load an RDF::Graph object from rdfxml in data attrib
168
+ # SIDE EFFECT: converts data to turtle for LdpWriter
169
+ # @return [RDF::Graph] populated RDF::Graph object, or nil
170
+ def rdfxml_to_graph
171
+ g = RDF::Graph.new.from_rdfxml(data)
172
+ g = nil if g && g.size == 0
173
+ self.data = g.dump(:ttl) if g # LdpWriter expects ttl
174
+ g
175
+ end
176
+
177
+ # avoid external lookup of jsonld context by putting it inline
178
+ # @return [Hash] the parsed json after the context is put inline
138
179
  def json_ld
139
180
  if data.match(/"@context"\s*\:\s*"http\:\/\/www\.w3\.org\/ns\/oa-context-20130208\.json"/)
140
181
  data.sub!("\"http://www.w3.org/ns/oa-context-20130208.json\"", Triannon::JsonldContext.oa_context)
@@ -35,9 +35,9 @@ module Triannon
35
35
 
36
36
  # @return json-ld representation of graph with OpenAnnotation context as a url
37
37
  def jsonld_oa
38
- inline_context = @graph.dump(:jsonld, :context => Triannon::JsonldContext::OA_CONTEXT_URL)
38
+ inline_context = @graph.dump(:jsonld, :context => Triannon::JsonldContext::OA_DATED_CONTEXT_URL)
39
39
  hash_from_json = JSON.parse(inline_context)
40
- hash_from_json["@context"] = Triannon::JsonldContext::OA_CONTEXT_URL
40
+ hash_from_json["@context"] = Triannon::JsonldContext::OA_DATED_CONTEXT_URL
41
41
  hash_from_json.to_json
42
42
  end
43
43
 
@@ -1,3 +1,3 @@
1
1
  module Triannon
2
- VERSION = "0.5.3"
2
+ VERSION = "0.5.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: triannon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Beer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-02-10 00:00:00.000000000 Z
13
+ date: 2015-03-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -315,7 +315,7 @@ files:
315
315
  - lib/triannon/version.rb
316
316
  homepage:
317
317
  licenses:
318
- - Apache 2
318
+ - Apache-2.0
319
319
  metadata: {}
320
320
  post_install_message:
321
321
  rdoc_options: []