tpitale-octopi 0.3.0
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 +5 -0
- data/.yardoc +0 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +20 -0
- data/README.markdown +137 -0
- data/Rakefile +91 -0
- data/VERSION.yml +4 -0
- data/contrib/backup.rb +100 -0
- data/examples/authenticated.rb +20 -0
- data/examples/issues.rb +18 -0
- data/examples/overall.rb +50 -0
- data/lib/ext/string_ext.rb +5 -0
- data/lib/octopi.rb +92 -0
- data/lib/octopi/api.rb +213 -0
- data/lib/octopi/base.rb +115 -0
- data/lib/octopi/blob.rb +25 -0
- data/lib/octopi/branch.rb +31 -0
- data/lib/octopi/branch_set.rb +11 -0
- data/lib/octopi/comment.rb +20 -0
- data/lib/octopi/commit.rb +69 -0
- data/lib/octopi/error.rb +35 -0
- data/lib/octopi/file_object.rb +16 -0
- data/lib/octopi/gist.rb +28 -0
- data/lib/octopi/issue.rb +111 -0
- data/lib/octopi/issue_comment.rb +7 -0
- data/lib/octopi/issue_set.rb +21 -0
- data/lib/octopi/key.rb +25 -0
- data/lib/octopi/key_set.rb +14 -0
- data/lib/octopi/plan.rb +5 -0
- data/lib/octopi/repository.rb +130 -0
- data/lib/octopi/repository_set.rb +9 -0
- data/lib/octopi/resource.rb +70 -0
- data/lib/octopi/self.rb +33 -0
- data/lib/octopi/tag.rb +23 -0
- data/lib/octopi/user.rb +131 -0
- data/test/api_test.rb +58 -0
- data/test/authenticated_test.rb +38 -0
- data/test/base_test.rb +20 -0
- data/test/blob_test.rb +23 -0
- data/test/branch_test.rb +20 -0
- data/test/commit_test.rb +82 -0
- data/test/file_object_test.rb +39 -0
- data/test/gist_test.rb +16 -0
- data/test/issue_comment.rb +19 -0
- data/test/issue_set_test.rb +33 -0
- data/test/issue_test.rb +120 -0
- data/test/key_set_test.rb +29 -0
- data/test/key_test.rb +35 -0
- data/test/repository_set_test.rb +23 -0
- data/test/repository_test.rb +151 -0
- data/test/stubs/commits/fcoury/octopi/octopi.rb +818 -0
- data/test/tag_test.rb +20 -0
- data/test/test_helper.rb +246 -0
- data/test/user_test.rb +92 -0
- data/tpitale-octopi.gemspec +99 -0
- metadata +142 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
module Octopi
|
2
|
+
module Resource
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.set_resource_name(base.name)
|
6
|
+
(@@resources||={})[base.resource_name(:singular)] = base
|
7
|
+
(@@resources||={})[base.resource_name(:plural)] = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.for(name)
|
11
|
+
@@resources[name]
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def set_resource_name(singular, plural = "#{singular}s")
|
16
|
+
@resource_name = {:singular => declassify(singular), :plural => declassify(plural)}
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource_name(key)
|
20
|
+
@resource_name[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_path(path)
|
24
|
+
(@path_spec||={})[:create] = path
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_path(path)
|
28
|
+
(@path_spec||={})[:find] = path
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_path(path)
|
32
|
+
(@path_spec||={})[:resource] = path
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_path(path)
|
36
|
+
(@path_spec||={})[:delete] = path
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(*args)
|
40
|
+
args = args.join('/') if args.is_a? Array
|
41
|
+
result = Api.api.find(path_for(:resource), @resource_name[:singular], args, self, @cache)
|
42
|
+
key = result.keys.first
|
43
|
+
|
44
|
+
if result[key].is_a? Array
|
45
|
+
result[key].map { |r| new(r) }
|
46
|
+
else
|
47
|
+
Resource.for(key).new(result[key])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_all(*s)
|
52
|
+
find_plural(s, :find)
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_plural(s, path)
|
56
|
+
s = s.join('/') if s.is_a? Array
|
57
|
+
resources = Api.api.find_all(path_for(path), @resource_name[:plural], s, self)
|
58
|
+
resources.map { |item| self.new(item) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def declassify(s)
|
62
|
+
(s.split('::').last || '').downcase if s
|
63
|
+
end
|
64
|
+
|
65
|
+
def path_for(type)
|
66
|
+
@path_spec[type]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/octopi/self.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Octopi
|
2
|
+
module Self
|
3
|
+
# Returns a list of Key objects containing all SSH Public Keys this user
|
4
|
+
# currently has. Requires authentication.
|
5
|
+
def keys
|
6
|
+
raise AuthenticationRequired, "To view keys, you must be authenticated" if Api.api.read_only?
|
7
|
+
result = Api.api.get("/user/keys", { :cache => false })
|
8
|
+
return unless result and result["public_keys"]
|
9
|
+
KeySet.new(result["public_keys"].inject([]) { |result, element| result << Key.new(element) })
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns a list of Email objects containing the email addresses associated with this user.
|
13
|
+
# Requires authentication.
|
14
|
+
def emails
|
15
|
+
raise AuthenticationRequired, "To view emails, you must be authenticated" if Api.api.read_only?
|
16
|
+
get("/user/emails")['emails']
|
17
|
+
end
|
18
|
+
|
19
|
+
# Start following a user.
|
20
|
+
# Can only be called if you are authenticated.
|
21
|
+
def follow!(login)
|
22
|
+
raise AuthenticationRequired, "To begin following someone, you must be authenticated" if Api.api.read_only?
|
23
|
+
Api.api.post("/user/follow/#{login}")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Stop following a user.
|
27
|
+
# Can only be called if you are authenticated.
|
28
|
+
def unfollow!(login)
|
29
|
+
raise AuthenticationRequired, "To stop following someone, you must be authenticated" if Api.api.read_only?
|
30
|
+
Api.api.post("/user/unfollow/#{login}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/octopi/tag.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Octopi
|
2
|
+
class Tag < Base
|
3
|
+
include Resource
|
4
|
+
|
5
|
+
attr_accessor :name, :sha
|
6
|
+
set_resource_name "tag"
|
7
|
+
|
8
|
+
resource_path "/repos/show/:id"
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
args = args.flatten!
|
12
|
+
self.name = args.first
|
13
|
+
self.sha = args.last
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.all(options={})
|
17
|
+
ensure_hash(options)
|
18
|
+
user, repo = gather_details(options)
|
19
|
+
self.validate_args(user => :user, repo => :repo)
|
20
|
+
find_plural([user, repo, 'tags'], :resource) { |i| Tag.new(i) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/octopi/user.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
module Octopi
|
2
|
+
class User < Base
|
3
|
+
include Resource
|
4
|
+
attr_accessor :company, :name, :following_count, :gravatar_id,
|
5
|
+
:blog, :public_repo_count, :public_gist_count,
|
6
|
+
:id, :login, :followers_count, :created_at,
|
7
|
+
:email, :location, :disk_usage, :private_repo_count,
|
8
|
+
:private_gist_count, :collaborators, :plan,
|
9
|
+
:owned_private_repo_count, :total_private_repo_count,
|
10
|
+
|
11
|
+
# These come from search results, which doesn't
|
12
|
+
# contain the above information.
|
13
|
+
:actions, :score, :language, :followers, :following,
|
14
|
+
:fullname, :type, :username, :repos, :pushed, :created
|
15
|
+
|
16
|
+
def plan=(attributes={})
|
17
|
+
@plan = Plan.new(attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
find_path "/user/search/:query"
|
21
|
+
resource_path "/user/show/:id"
|
22
|
+
|
23
|
+
# Finds a single user identified by the given username
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
#
|
27
|
+
# user = User.find("fcoury")
|
28
|
+
# puts user.login # should return 'fcoury'
|
29
|
+
def self.find(username)
|
30
|
+
self.validate_args(username => :user)
|
31
|
+
super username
|
32
|
+
end
|
33
|
+
|
34
|
+
# Finds all users whose username matches a given string
|
35
|
+
#
|
36
|
+
# Example:
|
37
|
+
#
|
38
|
+
# User.find_all("oe") # Matches joe, moe and monroe
|
39
|
+
#
|
40
|
+
def self.find_all(username)
|
41
|
+
self.validate_args(username => :user)
|
42
|
+
super username
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
alias_method :search, :find_all
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a collection of Repository objects, containing
|
50
|
+
# all repositories of the user.
|
51
|
+
#
|
52
|
+
# If user is the current authenticated user, some
|
53
|
+
# additional information will be provided for the
|
54
|
+
# Repositories.
|
55
|
+
def repositories
|
56
|
+
rs = RepositorySet.new(Repository.find(:user => self.login))
|
57
|
+
rs.user = self
|
58
|
+
rs
|
59
|
+
end
|
60
|
+
|
61
|
+
# Searches for user Repository identified by name
|
62
|
+
def repository(options={})
|
63
|
+
options = { :name => options } if options.is_a?(String)
|
64
|
+
self.class.ensure_hash(options)
|
65
|
+
Repository.find({ :user => login }.merge!(options))
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_repository(name, options = {})
|
69
|
+
self.class.validate_args(name => :repo)
|
70
|
+
Repository.create(self, name, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def watching
|
74
|
+
repositories = []
|
75
|
+
Api.api.get("/repos/watched/#{login}")["repositories"].each do |repo|
|
76
|
+
repositories << Repository.new(repo)
|
77
|
+
end
|
78
|
+
repositories
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Gets a list of followers.
|
83
|
+
# Returns an array of logins.
|
84
|
+
def followers
|
85
|
+
user_property("followers")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Gets a list of followers.
|
89
|
+
# Returns an array of user objects.
|
90
|
+
# If user has a large number of followers you may be rate limited by the API.
|
91
|
+
def followers!
|
92
|
+
user_property("followers", true)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Gets a list of people this user is following.
|
96
|
+
# Returns an array of logins.
|
97
|
+
def following
|
98
|
+
user_property("following")
|
99
|
+
end
|
100
|
+
|
101
|
+
# Gets a list of people this user is following.
|
102
|
+
# Returns an array of user objectrs.
|
103
|
+
# If user has a large number of people whom they follow, you may be rate limited by the API.
|
104
|
+
def following!
|
105
|
+
user_property("following", true)
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# If a user object is passed into a method, we can use this.
|
110
|
+
# It'll also work if we pass in just the login.
|
111
|
+
def to_s
|
112
|
+
login
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# Helper method for "deep" finds.
|
118
|
+
# Determines whether to return an array of logins (light) or user objects (heavy).
|
119
|
+
def user_property(property, deep=false)
|
120
|
+
users = []
|
121
|
+
property(property, login).each_pair do |k,v|
|
122
|
+
return v unless deep
|
123
|
+
|
124
|
+
v.each { |u| users << User.find(u) }
|
125
|
+
end
|
126
|
+
|
127
|
+
users
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
data/test/api_test.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class AuthenticatedTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
|
6
|
+
def setup
|
7
|
+
fake_everything
|
8
|
+
@user = User.find("fcoury")
|
9
|
+
end
|
10
|
+
|
11
|
+
context "following" do
|
12
|
+
|
13
|
+
should "not be able to follow anyone if not authenticated" do
|
14
|
+
exception = assert_raise AuthenticationRequired do
|
15
|
+
Api.me.follow!("rails")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
should "be able to follow a user" do
|
20
|
+
auth do
|
21
|
+
assert_not_nil Api.me.follow!("rails")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "unfollowing" do
|
27
|
+
|
28
|
+
should "not be able to follow anyone if not authenticated" do
|
29
|
+
exception = assert_raise AuthenticationRequired do
|
30
|
+
Api.me.unfollow!("rails")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
should "be able to follow a user" do
|
35
|
+
auth do
|
36
|
+
assert_not_nil Api.me.unfollow!("rails")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "keys" do
|
42
|
+
should "not be able to see keys if not authenticated" do
|
43
|
+
exception = assert_raise AuthenticationRequired do
|
44
|
+
Api.me.keys
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal "To view keys, you must be authenticated", exception.message
|
48
|
+
end
|
49
|
+
|
50
|
+
should "have some keys" do
|
51
|
+
auth do
|
52
|
+
keys = Api.me.keys
|
53
|
+
assert keys.is_a?(KeySet)
|
54
|
+
assert_equal 2, keys.size
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class AuthenticatedTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
def setup
|
6
|
+
fake_everything
|
7
|
+
end
|
8
|
+
|
9
|
+
context "Authenticating" do
|
10
|
+
should "not be possible with username and password" do
|
11
|
+
assert_raises(RuntimeError) do
|
12
|
+
authenticated_with(:login => "fcoury", :password => "yruocf") {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
should "be possible with username and token" do
|
17
|
+
auth do
|
18
|
+
assert_not_nil User.find("fcoury")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
should "be possible using the .gitconfig" do
|
23
|
+
authenticated do
|
24
|
+
assert_not_nil User.find("fcoury")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
#
|
28
|
+
# should "be denied access when specifying an invalid token and login combination" do
|
29
|
+
# FakeWeb.clean_registry
|
30
|
+
# FakeWeb.register_uri(:get, "http://github.com/api/v2/yaml/user/show/fcoury", :status => ["404", "Not Found"])
|
31
|
+
# assert_raise InvalidLogin do
|
32
|
+
# authenticated_with :login => "fcoury", :token => "ba7bf2d7f0ebc073d3874dda887b18ae" do
|
33
|
+
# # Just blank will do us fine.
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
end
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class BaseTest < Test::Unit::TestCase
|
4
|
+
class SparseUser < Octopi::Base
|
5
|
+
include Octopi::Resource
|
6
|
+
|
7
|
+
attr_accessor :some_attribute
|
8
|
+
|
9
|
+
find_path "/user/search/:query"
|
10
|
+
resource_path "/user/show/:id"
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup
|
14
|
+
fake_everything
|
15
|
+
end
|
16
|
+
|
17
|
+
should "not raise an error if it doesn't know about the attributes that GitHub API provides" do
|
18
|
+
assert_nothing_raised { SparseUser.find("radar") }
|
19
|
+
end
|
20
|
+
end
|
data/test/blob_test.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class BlobTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
|
6
|
+
def setup
|
7
|
+
fake_everything
|
8
|
+
end
|
9
|
+
|
10
|
+
context Blob do
|
11
|
+
should "find a blob" do
|
12
|
+
blob = Blob.find(:user => "fcoury", :repo => "octopi", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae")
|
13
|
+
assert_not_nil blob
|
14
|
+
assert blob.is_a?(String)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Can't grab real data for this yet, Github is throwing a 500 on this request.
|
18
|
+
# should "find a file for a blob" do
|
19
|
+
# assert_not_nil Blob.find(:user => "fcoury", :repo => "octopi", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae", :path => "lib/octopi.rb")
|
20
|
+
# end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/test/branch_test.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class BranchTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
|
6
|
+
def setup
|
7
|
+
fake_everything
|
8
|
+
end
|
9
|
+
|
10
|
+
context Branch do
|
11
|
+
should "Find all branches for a repository" do
|
12
|
+
assert_not_nil Branch.all(:user => "fcoury", :name => "octopi")
|
13
|
+
end
|
14
|
+
|
15
|
+
should "Be able to find a specific branch" do
|
16
|
+
assert_not_nil Branch.all(:user => "fcoury", :name => "octopi").find("lazy")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/test/commit_test.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class CommitTest < Test::Unit::TestCase
|
4
|
+
include Octopi
|
5
|
+
|
6
|
+
def setup
|
7
|
+
fake_everything
|
8
|
+
@user = User.find("fcoury")
|
9
|
+
@repo = @user.repository(:name => "octopi")
|
10
|
+
end
|
11
|
+
|
12
|
+
context Commit do
|
13
|
+
context "finding all commits" do
|
14
|
+
should "by strings" do
|
15
|
+
commits = Commit.find_all(:user => "fcoury", :repository => "octopi")
|
16
|
+
assert_not_nil commits
|
17
|
+
assert_equal 30, commits.size
|
18
|
+
assert_not_nil commits.first.repository
|
19
|
+
end
|
20
|
+
|
21
|
+
should "by objects" do
|
22
|
+
commits = Commit.find_all(:user => @user, :repository => @repo)
|
23
|
+
assert_not_nil commits
|
24
|
+
assert_equal 30, commits.size
|
25
|
+
end
|
26
|
+
|
27
|
+
should "be able to specify a branch" do
|
28
|
+
commits = Commit.find_all(:user => "fcoury", :repository => "octopi", :branch => "lazy")
|
29
|
+
assert_not_nil commits
|
30
|
+
assert_equal 30, commits.size
|
31
|
+
end
|
32
|
+
|
33
|
+
# Tests issue #28
|
34
|
+
should "be able to find commits in a private repository" do
|
35
|
+
auth do
|
36
|
+
commits = Commit.find_all(:user => "fcoury", :repository => "rboard")
|
37
|
+
end
|
38
|
+
assert_not_nil commits
|
39
|
+
assert_equal 22, commits.size
|
40
|
+
end
|
41
|
+
|
42
|
+
should "be able to find commits for a particular file" do
|
43
|
+
commits = Commit.find_all(:user => "fcoury", :repository => "octopi", :path => "lib/octopi.rb")
|
44
|
+
assert_not_nil commits
|
45
|
+
assert_equal 44, commits.size
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "finding a single commit" do
|
50
|
+
should "by strings" do
|
51
|
+
commit = Commit.find(:name => "octopi", :user => "fcoury", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae")
|
52
|
+
assert_not_nil commit
|
53
|
+
end
|
54
|
+
|
55
|
+
should "by objects" do
|
56
|
+
commit = Commit.find(:name => "octopi", :user => "fcoury", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae")
|
57
|
+
assert_not_nil commit
|
58
|
+
end
|
59
|
+
|
60
|
+
should "be able to specify a branch" do
|
61
|
+
commit = Commit.find(:name => "octopi", :user => "fcoury", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae", :branch => "lazy")
|
62
|
+
assert_not_nil commit
|
63
|
+
end
|
64
|
+
|
65
|
+
should "raise an error if not found" do
|
66
|
+
exception = assert_raise Octopi::NotFound do
|
67
|
+
Commit.find(:name => "octopi", :user => "fcoury", :sha => "nothere")
|
68
|
+
end
|
69
|
+
|
70
|
+
assert_equal "The Commit you were looking for could not be found, or is private.", exception.message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "identifying a repository" do
|
75
|
+
should "be possible" do
|
76
|
+
commit = Commit.find(:name => "octopi", :user => "fcoury", :sha => "f6609209c3ac0badd004512d318bfaa508ea10ae")
|
77
|
+
assert_equal "fcoury/octopi/f6609209c3ac0badd004512d318bfaa508ea10ae", commit.repo_identifier
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|