opensearch 0.0.1
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/CHANGES +6 -0
- data/README +31 -0
- data/Rakefile +154 -0
- data/lib/opensearch.rb +57 -0
- data/lib/opensearch/1.0.rb +46 -0
- data/lib/opensearch/1.1.rb +103 -0
- data/lib/opensearch/base.rb +70 -0
- metadata +56 -0
data/CHANGES
ADDED
data/README
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
= OpenSearch
|
2
|
+
|
3
|
+
Ruby/OpenSearch - Search A9 OpenSearch compatible engines
|
4
|
+
|
5
|
+
OpenSearch is simple format of sharing of search results by A9. See http://opensearch.a9.com/ for detail.
|
6
|
+
|
7
|
+
This library is for OpenSearch version 1.0 or 1.1
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require 'opensearch'
|
13
|
+
|
14
|
+
# initialize
|
15
|
+
engine = OpenSearch::OpenSearch.new "http://search.hatena.ne.jp/osxml"
|
16
|
+
|
17
|
+
# get information of Description Document
|
18
|
+
name = engine.short_name
|
19
|
+
tags = engine.tags
|
20
|
+
|
21
|
+
# search (retrun value is RSS::Rss)
|
22
|
+
feed = engine.search("some text")
|
23
|
+
|
24
|
+
# OpenSearch Version 1.1
|
25
|
+
feed = engine.search("some text", "type") # type is like "application/rss+xml"
|
26
|
+
|
27
|
+
== Author
|
28
|
+
- drawnboy ( http://nowherenear.net ) <drawn.boy@gmail.com.nospam>
|
29
|
+
|
30
|
+
== License
|
31
|
+
- Same as Ruby.
|
data/Rakefile
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# Rakefile for MetaProject
|
2
|
+
|
3
|
+
# Copyright 2005 by Aslak Hellesoy (aslak.hellesoy@gmail.org)
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
# This file is may be distributed under an MIT style license. See
|
7
|
+
# MIT-LICENSE for details.
|
8
|
+
|
9
|
+
$:.unshift('lib')
|
10
|
+
require 'meta_project'
|
11
|
+
require 'rake/gempackagetask'
|
12
|
+
require 'rake/contrib/rubyforgepublisher'
|
13
|
+
require 'rake/contrib/xforge'
|
14
|
+
require 'rake/clean'
|
15
|
+
require 'rake/testtask'
|
16
|
+
require 'rake/rdoctask'
|
17
|
+
|
18
|
+
# Versioning scheme: MAJOR.MINOR.PATCH
|
19
|
+
# MAJOR bumps when API is broken backwards
|
20
|
+
# MINOR bumps when the API is broken backwards in a very slight/subtle (but not fatal) way
|
21
|
+
# -OR when a new release is made and propaganda is sent out.
|
22
|
+
# PATCH is bumped for every API addition and/or bugfix (ideally for every commit)
|
23
|
+
# Later DamageControl can bump PATCH automatically.
|
24
|
+
#
|
25
|
+
# REMEMBER TO KEEP PKG_VERSION IN SYNC WITH THE CHANGES FILE!
|
26
|
+
PKG_NAME = "opensearch"
|
27
|
+
PKG_VERSION = "0.0.1"
|
28
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
29
|
+
PKG_FILES = FileList[
|
30
|
+
'[A-Z]*',
|
31
|
+
'lib/**/*.rb',
|
32
|
+
'doc/**/*'
|
33
|
+
]
|
34
|
+
|
35
|
+
task :default => [:gem]
|
36
|
+
|
37
|
+
desc "Execute RSpec specifications (rspec gem must be installed)"
|
38
|
+
task :spec do
|
39
|
+
ruby 'behaviours/suite.rb'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a task to build the RDOC documentation tree.
|
43
|
+
rd = Rake::RDocTask.new("rdoc") do |rdoc|
|
44
|
+
rdoc.rdoc_dir = 'html'
|
45
|
+
# rdoc.template = 'kilmer'
|
46
|
+
# rdoc.template = 'css2'
|
47
|
+
# rdoc.template = 'doc/jamis.rb'
|
48
|
+
rdoc.title = "opensearch"
|
49
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
50
|
+
rdoc.rdoc_files.include('README', 'CHANGES')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb', 'doc/**/*.rdoc')
|
52
|
+
rdoc.rdoc_files.exclude('doc/**/*_attrs.rdoc')
|
53
|
+
end
|
54
|
+
|
55
|
+
# ====================================================================
|
56
|
+
# Create a task that will package the Rake software into distributable
|
57
|
+
# tar, zip and gem files.
|
58
|
+
|
59
|
+
spec = Gem::Specification.new do |s|
|
60
|
+
|
61
|
+
#### Basic information.
|
62
|
+
|
63
|
+
s.name = PKG_NAME
|
64
|
+
s.version = PKG_VERSION
|
65
|
+
s.summary = "Search A9 OpenSearch compatible engines"
|
66
|
+
s.description = <<-EOF
|
67
|
+
OpenSearch is simple format of sharing of search results by A9. See opensearch.a9.com/ for detail.
|
68
|
+
|
69
|
+
This library is for OpenSearch version 1.0 or 1.1
|
70
|
+
EOF
|
71
|
+
|
72
|
+
s.files = PKG_FILES.to_a
|
73
|
+
s.require_path = 'lib'
|
74
|
+
#s.autorequire = 'meta_project'
|
75
|
+
|
76
|
+
#### Documentation and testing.
|
77
|
+
|
78
|
+
s.has_rdoc = true
|
79
|
+
s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
|
80
|
+
s.rdoc_options <<
|
81
|
+
'--title' << 'OpenSearch' <<
|
82
|
+
'--main' << 'README' <<
|
83
|
+
'--line-numbers'
|
84
|
+
|
85
|
+
#### Author and project details.
|
86
|
+
|
87
|
+
s.author = "drawnboy"
|
88
|
+
s.email = "drawn.boy@gmail.com"
|
89
|
+
s.homepage = "http://opensearch.rubyforge.org"
|
90
|
+
s.rubyforge_project = "opensearch"
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "Build Gem"
|
94
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
95
|
+
pkg.need_zip = true
|
96
|
+
pkg.need_tar = true
|
97
|
+
end
|
98
|
+
|
99
|
+
# Support Tasks ------------------------------------------------------
|
100
|
+
|
101
|
+
desc "Look for TODO and FIXME tags in the code"
|
102
|
+
task :todo do
|
103
|
+
Pathname.new(File.dirname(__FILE__)).egrep(/#.*(FIXME|TODO|TBD|DEPRECATED)/) do |match|
|
104
|
+
puts match
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
task :release => [:verify_env_vars, :release_files, :publish_doc, :publish_news, :tag]
|
109
|
+
|
110
|
+
task :verify_env_vars do
|
111
|
+
raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER']
|
112
|
+
raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD']
|
113
|
+
end
|
114
|
+
|
115
|
+
task :publish_doc do
|
116
|
+
publisher = Rake::RubyForgePublisher.new('opensearch', ENV['RUBYFORGE_USER'])
|
117
|
+
publisher.upload
|
118
|
+
end
|
119
|
+
|
120
|
+
desc "Release files on RubyForge"
|
121
|
+
task :release_files => [:gem] do
|
122
|
+
release_files = FileList[
|
123
|
+
"pkg/#{PKG_FILE_NAME}.gem"
|
124
|
+
]
|
125
|
+
|
126
|
+
Rake::XForge::Release.new(MetaProject::Project::XForge::RubyForge.new('opensearch')) do |release|
|
127
|
+
# Never hardcode user name and password in the Rakefile!
|
128
|
+
release.user_name = ENV['RUBYFORGE_USER']
|
129
|
+
release.password = ENV['RUBYFORGE_PASSWORD']
|
130
|
+
release.files = release_files.to_a
|
131
|
+
release.release_name = "#{PKG_NAME} #{PKG_VERSION}"
|
132
|
+
# The rest of the options are defaults (among others, release_notes and release_changes, parsed from CHANGES)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
desc "Publish news on RubyForge"
|
137
|
+
task :publish_news => [:gem] do
|
138
|
+
release_files = FileList[
|
139
|
+
"pkg/#{PKG_FILE_NAME}.gem"
|
140
|
+
]
|
141
|
+
|
142
|
+
Rake::XForge::NewsPublisher.new(MetaProject::Project::XForge::RubyForge.new('opensearch')) do |news|
|
143
|
+
# Never hardcode user name and password in the Rakefile!
|
144
|
+
news.user_name = ENV['RUBYFORGE_USER']
|
145
|
+
news.password = ENV['RUBYFORGE_PASSWORD']
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
desc "Tag all the CVS files with the latest release number (REL=x.y.z)"
|
150
|
+
task :tag do
|
151
|
+
reltag = "REL_#{PKG_VERSION.gsub(/\./, '_')}"
|
152
|
+
puts "Tagging CVS with [#{reltag}]"
|
153
|
+
sh %{cvs tag #{reltag}}
|
154
|
+
end
|
data/lib/opensearch.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2006 drawnboy (drawn.boy@gmail.com)
|
3
|
+
#
|
4
|
+
# This program is free software.
|
5
|
+
# You can distribute/modify this program under the terms of the Ruby License.
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'open-uri'
|
9
|
+
require 'rexml/document'
|
10
|
+
|
11
|
+
require 'opensearch/1.0'
|
12
|
+
require 'opensearch/1.1'
|
13
|
+
|
14
|
+
module OpenSearch
|
15
|
+
class OpenSearch
|
16
|
+
class << self
|
17
|
+
def new(url)
|
18
|
+
engine = nil
|
19
|
+
ns_uri, doc = fetch_description url
|
20
|
+
|
21
|
+
case ns_uri
|
22
|
+
when %r"http://a9.com/-/spec/opensearch(rss|)/1.0/"
|
23
|
+
engine = OpenSearch10.new doc
|
24
|
+
when %r"http://a9.com/-/spec/opensearch/1.1/"
|
25
|
+
engine = OpenSearch11.new doc
|
26
|
+
end
|
27
|
+
|
28
|
+
raise "Cannot detect description of opensearch version 1.0 or 1.1" if engine.nil?
|
29
|
+
engine
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def fetch_description(url)
|
34
|
+
content = open(url) {|f| content = f.read }
|
35
|
+
doc = REXML::Document.new content
|
36
|
+
ns_uri = nil
|
37
|
+
REXML::XPath.each(doc, "//Format") do |node|
|
38
|
+
ns_uri = node.text
|
39
|
+
end
|
40
|
+
if ns_uri.nil?
|
41
|
+
REXML::XPath.each(doc, "//OpenSearchDescription") do |node|
|
42
|
+
ns_uri = node.attributes.get_attribute("xmlns").to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
return ns_uri, doc
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if __FILE__ == $0
|
52
|
+
#engine = OpenSearch::OpenSearch.new "http://bulkfeeds.net/opensearch.xml"
|
53
|
+
engine = OpenSearch::OpenSearch.new "http://127.0.0.1/example.xml"
|
54
|
+
engine.set_custom("items_per_page", 20)
|
55
|
+
feed = engine.search("test", "text/html")
|
56
|
+
p feed
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'opensearch/base'
|
2
|
+
|
3
|
+
module OpenSearch
|
4
|
+
class OpenSearch10 < OpenSearchBase
|
5
|
+
Nodes = %w(url format short_name long_name description tags image sample_search developer contact attribution syndication_right adult_content)
|
6
|
+
Pagers = {
|
7
|
+
"count" => 20,
|
8
|
+
"start_index" => 1,
|
9
|
+
"start_page" => 1
|
10
|
+
}
|
11
|
+
|
12
|
+
def initialize(doc)
|
13
|
+
@description = Hash.new
|
14
|
+
@pager = Pagers.dup
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def search(query)
|
19
|
+
url = @description["url"]
|
20
|
+
rss = super(url, query)
|
21
|
+
parse_rss(rss)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def install_accessor
|
26
|
+
class << self
|
27
|
+
Nodes.each do |node|
|
28
|
+
define_method(node){ @description[node] }
|
29
|
+
end
|
30
|
+
|
31
|
+
Pagers.each_key do |pager|
|
32
|
+
define_method(pager){ @pager[pager] }
|
33
|
+
define_method("#{pager}="){|value| @pager[pager] = value }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def setup_description(doc)
|
39
|
+
Nodes.each do |node|
|
40
|
+
REXML::XPath.each(doc, "//#{node.gsub(/(^|_)(.)/){$2.upcase}}") do |n|
|
41
|
+
@description[node] = n.text
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'opensearch/base'
|
2
|
+
|
3
|
+
module OpenSearch
|
4
|
+
class OpenSearch11 < OpenSearch::OpenSearchBase
|
5
|
+
Nodes = {
|
6
|
+
"url" => { :format => {}, :requirements => [] },
|
7
|
+
"short_name" => { :format => "", :requirements => nil },
|
8
|
+
"long_name" => { :format => "", :requirements => nil },
|
9
|
+
"description" => { :format => "", :requirements => nil },
|
10
|
+
"tags" => { :format => "", :requirements => nil },
|
11
|
+
"image" => { :format => {}, :requirements => [] },
|
12
|
+
"query" => { :format => {}, :requirements => [] },
|
13
|
+
"developer" => { :format => "", :requirements => nil },
|
14
|
+
"contact" => { :format => "", :requirements => nil },
|
15
|
+
"attribution" => { :format => "", :requirements => nil },
|
16
|
+
"syndication_right" => { :format => "", :requirements => nil },
|
17
|
+
"adult_content" => { :format => "", :requirements => nil },
|
18
|
+
"language" => { :format => "", :requirements => [] },
|
19
|
+
"input_encoding" => { :format => "", :requirements => [] },
|
20
|
+
"output_encoding" => { :format => "", :requirements => [] },
|
21
|
+
}
|
22
|
+
Pagers = {
|
23
|
+
"count" => 20,
|
24
|
+
"start_index" => 1,
|
25
|
+
"start_page" => 1,
|
26
|
+
"language" => "*",
|
27
|
+
"output_encoding" => "UTF-8",
|
28
|
+
"input_encoding" => "UTF-8",
|
29
|
+
}
|
30
|
+
|
31
|
+
def initialize(doc)
|
32
|
+
@description = Hash.new
|
33
|
+
@pager = Pagers.dup
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def search(query, type = nil)
|
38
|
+
url = nil
|
39
|
+
post = false
|
40
|
+
if type.nil?
|
41
|
+
url = @description["url"][0]["template"]
|
42
|
+
else
|
43
|
+
@description["url"].each do |u|
|
44
|
+
if u["type"] == type
|
45
|
+
url = u["template"]
|
46
|
+
if u["method"] =~ /post/i
|
47
|
+
post = u["param"]
|
48
|
+
post = setup_query(post, query)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
raise "cannot find strict url from Description." if url.nil?
|
54
|
+
super(url, query, post)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def install_accessor
|
59
|
+
class << self
|
60
|
+
Nodes.each_key do |node|
|
61
|
+
define_method(node){ @description[node] }
|
62
|
+
end
|
63
|
+
|
64
|
+
Pagers.each_key do |pager|
|
65
|
+
define_method(pager){ @pager[pager] }
|
66
|
+
define_method("#{pager}="){|value| @pager[pager] = value }
|
67
|
+
define_method("set_custom"){|pager, value| @pager[pager] = value }
|
68
|
+
define_method("get_pager"){|pager| @pager[pager] }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def setup_description(doc)
|
74
|
+
Nodes.each_key do |node|
|
75
|
+
REXML::XPath.each(doc, "//#{node.gsub(/(^|_)(.)/){$2.upcase}}") do |n|
|
76
|
+
description = nil
|
77
|
+
if Nodes[node][:format].class == Hash
|
78
|
+
description = Hash.new
|
79
|
+
n.attributes.each do |key, value|
|
80
|
+
description[key] = value
|
81
|
+
end
|
82
|
+
description[node] = n.text unless n.text.nil?
|
83
|
+
if node == "url" && n.has_elements?
|
84
|
+
description["param"] = ""
|
85
|
+
REXML::XPath.each(n, "//Param") do |param|
|
86
|
+
param.attributes.each do |k, v|
|
87
|
+
description["param"] << "#{k}=#{v}&"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
else
|
92
|
+
description = n.text
|
93
|
+
end
|
94
|
+
if Nodes[node][:requirements].class == Array
|
95
|
+
@description[node] = @description[node].to_a.push(description)
|
96
|
+
else
|
97
|
+
@description[node] = description
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rexml/document'
|
4
|
+
require "rss/1.0"
|
5
|
+
require "rss/2.0"
|
6
|
+
require "rss/dublincore"
|
7
|
+
|
8
|
+
module OpenSearch
|
9
|
+
class OpenSearchBase
|
10
|
+
def initialize(doc)
|
11
|
+
install_accessor
|
12
|
+
setup_description doc
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def search(url, query, post = false)
|
17
|
+
query = setup_query(url, query)
|
18
|
+
post ? post_content(query, post) : get_content(query)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def install_accessor
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup_description(doc)
|
26
|
+
end
|
27
|
+
|
28
|
+
def setup_query(url, query)
|
29
|
+
search_terms = URI.escape(query)
|
30
|
+
url.gsub!("{searchTerms}", search_terms)
|
31
|
+
@pager.each do |key, value|
|
32
|
+
key = key.gsub(/_(.)/){$1.upcase}
|
33
|
+
url.gsub!(/\{#{key}(\?|)\}/, value.to_s)
|
34
|
+
end
|
35
|
+
url
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_content(uri)
|
39
|
+
uri = URI.parse(uri)
|
40
|
+
Net::HTTP.version_1_2
|
41
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
42
|
+
response = http.get("#{uri.path}?#{uri.query}")
|
43
|
+
raise "Get Error : #{response.code} - #{response.message}" unless response.code == "200"
|
44
|
+
response.body
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def post_content(uri, data)
|
49
|
+
uri = URI.parse(uri)
|
50
|
+
Net::HTTP.version_1_2
|
51
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
52
|
+
response = http.get("#{uri.path}?#{uri.query}", data)
|
53
|
+
raise "Post Error : #{response.code} - #{response.message}" unless response.code == "200"
|
54
|
+
response.body
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_rss(rss)
|
59
|
+
begin
|
60
|
+
RSS::Parser.parse(rss)
|
61
|
+
rescue RSS::InvalidRSSError
|
62
|
+
RSS::Parser.parse(rss, false)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
## No implementation of atom format parser, not yet...
|
67
|
+
# def atom_parser(xml)
|
68
|
+
# end
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: opensearch
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-01-11 00:00:00 +09:00
|
8
|
+
summary: Search A9 OpenSearch compatible engines
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: drawn.boy@gmail.com
|
12
|
+
homepage: http://opensearch.rubyforge.org
|
13
|
+
rubyforge_project: opensearch
|
14
|
+
description: OpenSearch is simple format of sharing of search results by A9. See opensearch.a9.com/ for detail. This library is for OpenSearch version 1.0 or 1.1
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- drawnboy
|
30
|
+
files:
|
31
|
+
- README
|
32
|
+
- Rakefile
|
33
|
+
- CHANGES
|
34
|
+
- lib/opensearch.rb
|
35
|
+
- lib/opensearch/1.1.rb
|
36
|
+
- lib/opensearch/base.rb
|
37
|
+
- lib/opensearch/1.0.rb
|
38
|
+
test_files: []
|
39
|
+
|
40
|
+
rdoc_options:
|
41
|
+
- --title
|
42
|
+
- OpenSearch
|
43
|
+
- --main
|
44
|
+
- README
|
45
|
+
- --line-numbers
|
46
|
+
extra_rdoc_files:
|
47
|
+
- README
|
48
|
+
- CHANGES
|
49
|
+
executables: []
|
50
|
+
|
51
|
+
extensions: []
|
52
|
+
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
dependencies: []
|
56
|
+
|