hallon 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +6 -0
- data/.gemtest +0 -0
- data/.gitignore +29 -0
- data/.rspec +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG +20 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/QUIRKS +11 -0
- data/README.markdown +58 -0
- data/Rakefile +75 -0
- data/Termfile +7 -0
- data/examples/logging_in.rb +26 -0
- data/examples/printing_link_information.rb +27 -0
- data/hallon.gemspec +31 -0
- data/lib/hallon.rb +34 -0
- data/lib/hallon/error.rb +54 -0
- data/lib/hallon/ext/ffi.rb +26 -0
- data/lib/hallon/ext/spotify.rb +101 -0
- data/lib/hallon/image.rb +70 -0
- data/lib/hallon/link.rb +101 -0
- data/lib/hallon/linkable.rb +50 -0
- data/lib/hallon/observable.rb +91 -0
- data/lib/hallon/session.rb +189 -0
- data/lib/hallon/synchronizable.rb +32 -0
- data/lib/hallon/user.rb +69 -0
- data/lib/hallon/version.rb +7 -0
- data/spec/fixtures/example_uris.rb +11 -0
- data/spec/fixtures/pink_cover.jpg +0 -0
- data/spec/hallon/error_spec.rb +30 -0
- data/spec/hallon/ffi_spec.rb +5 -0
- data/spec/hallon/hallon_spec.rb +16 -0
- data/spec/hallon/image_spec.rb +41 -0
- data/spec/hallon/link_spec.rb +84 -0
- data/spec/hallon/linkable_spec.rb +43 -0
- data/spec/hallon/observable_spec.rb +103 -0
- data/spec/hallon/session_spec.rb +61 -0
- data/spec/hallon/synchronizable_spec.rb +19 -0
- data/spec/hallon/user_spec.rb +73 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/support/.gitkeep +0 -0
- data/spec/support/context_initialized_session.rb +3 -0
- data/spec/support/context_logged_in.rb +16 -0
- data/spec/support/cover_me.rb +5 -0
- data/spec/support/shared_for_loadable_objects.rb +7 -0
- metadata +271 -96
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'monitor'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Hallon
|
6
|
+
# Adds synchronization primitives to target when included.
|
7
|
+
module Synchronizable
|
8
|
+
# Creates a `Monitor` for the target instance and adds `monitor` class method for access.
|
9
|
+
#
|
10
|
+
# Also adds several other methods:
|
11
|
+
#
|
12
|
+
# - `#synchronize`
|
13
|
+
# - `#new_cond`
|
14
|
+
#
|
15
|
+
# These all delegate to `#monitor`.
|
16
|
+
#
|
17
|
+
# @note This module is part of Hallons private API
|
18
|
+
# @private
|
19
|
+
def self.included(o)
|
20
|
+
o.instance_exec do
|
21
|
+
@monitor = Monitor.new
|
22
|
+
class << self
|
23
|
+
attr_reader :monitor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
extend Forwardable
|
29
|
+
def_delegators :monitor, :synchronize, :new_cond
|
30
|
+
def_delegators 'self.class', :monitor
|
31
|
+
end
|
32
|
+
end
|
data/lib/hallon/user.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Hallon
|
3
|
+
# Users are the entities that interact with the Spotify service.
|
4
|
+
#
|
5
|
+
# Methods are available for retrieving metadata and relationship
|
6
|
+
# status between users.
|
7
|
+
#
|
8
|
+
# @see http://developer.spotify.com/en/libspotify/docs/group__user.html
|
9
|
+
class User
|
10
|
+
extend Linkable
|
11
|
+
|
12
|
+
# @macro [attach] from_link
|
13
|
+
# Given a Link, get its’ underlying pointer.
|
14
|
+
#
|
15
|
+
# @method to_link
|
16
|
+
# @scope class
|
17
|
+
# @param [String, Hallon::Link, FFI::Pointer] link
|
18
|
+
# @return [FFI::Pointer]
|
19
|
+
from_link(:profile) { |link| Spotify::link_as_user(link) }
|
20
|
+
|
21
|
+
# @macro [attach] to_link
|
22
|
+
# Create a Link to the current object.
|
23
|
+
#
|
24
|
+
# @method to_link
|
25
|
+
# @scope instance
|
26
|
+
# @return [Hallon::Link]
|
27
|
+
to_link(:user)
|
28
|
+
|
29
|
+
# Used by {Session#relation_type?}
|
30
|
+
attr_reader :pointer
|
31
|
+
|
32
|
+
# Construct a new instance of User.
|
33
|
+
#
|
34
|
+
# @param [String, Link, FFI::Pointer] link
|
35
|
+
def initialize(link)
|
36
|
+
@pointer = Spotify::Pointer.new from_link(link), :user, true
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] true if the user is loaded
|
40
|
+
def loaded?
|
41
|
+
Spotify::user_is_loaded(@pointer)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Retrieve the name of the current user.
|
45
|
+
#
|
46
|
+
# @note Unless the user is {User#loaded?} only the canonical name is accessible
|
47
|
+
# @param [Symbol] type one of :canonical, :display, :full
|
48
|
+
# @return [String]
|
49
|
+
def name(type = :canonical)
|
50
|
+
case type
|
51
|
+
when :display
|
52
|
+
Spotify::user_display_name(@pointer)
|
53
|
+
when :full
|
54
|
+
Spotify::user_full_name(@pointer)
|
55
|
+
when :canonical
|
56
|
+
Spotify::user_canonical_name(@pointer)
|
57
|
+
else
|
58
|
+
raise ArgumentError, "expected type to be :display, :full or :canonical, but was #{type}"
|
59
|
+
end.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Retrieve the URL to the users’ profile picture.
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
def picture
|
66
|
+
Spotify::user_picture(@pointer).to_s
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
def example_uris
|
2
|
+
{
|
3
|
+
"spotify:search:omg%2bwtf%2b%ef%a3%bf%c3%9f%e2%88%82%2bbbq" => :search,
|
4
|
+
"spotify:track:3oN2Kq1h07LSSBSLYQp0Ns" => :track,
|
5
|
+
"spotify:album:6I58XCEkOnfUVsfpDehzlQ" => :album,
|
6
|
+
"spotify:artist:6MF9fzBmfXghAz953czmBC" => :artist,
|
7
|
+
"spotify:user:burgestrand:playlist:4nQnbGi4kALbME9csEqdW2" => :playlist,
|
8
|
+
"spotify:user:burgestrand" => :profile,
|
9
|
+
"spotify:user:burgestrand:starred" => :starred,
|
10
|
+
}
|
11
|
+
end
|
Binary file
|
@@ -0,0 +1,30 @@
|
|
1
|
+
describe Hallon::Error do
|
2
|
+
subject { described_class }
|
3
|
+
|
4
|
+
it { should <= RuntimeError }
|
5
|
+
|
6
|
+
describe "::disambiguate" do
|
7
|
+
it "should not fail on invalid numbers" do
|
8
|
+
subject.disambiguate(10000).should eq [-1, nil]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should not fail on invalid symbols" do
|
12
|
+
subject.disambiguate(:fail).should eq [-1, nil]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "::explain" do
|
17
|
+
it { subject.explain(0).should eq 'No error' }
|
18
|
+
it { subject.explain(-1).should eq 'invalid error code' }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "::maybe_raise" do
|
22
|
+
it "should not raise error when given 0 as error code" do
|
23
|
+
expect { subject.maybe_raise(0) }.to_not raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should raise error when given non-0 as error code" do
|
27
|
+
expect { subject.maybe_raise(1) }.to raise_error(Hallon::Error)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
describe Hallon do
|
2
|
+
describe "VERSION" do
|
3
|
+
specify { Hallon::VERSION.should == "0.0.0" }
|
4
|
+
end
|
5
|
+
|
6
|
+
describe "API_VERSION" do
|
7
|
+
specify { Hallon::API_VERSION.should == 8 }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "URI" do
|
11
|
+
subject { Hallon::URI }
|
12
|
+
example_uris.keys.each do |uri|
|
13
|
+
it { should match uri }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
describe Hallon::Image, :logged_in => true do
|
3
|
+
context "created from a Spotify URI" do
|
4
|
+
before(:all) do
|
5
|
+
@uri = "spotify:image:3ad93423add99766e02d563605c6e76ed2b0e450"
|
6
|
+
@image = Hallon::Image.new @uri
|
7
|
+
@image.status.should eq :is_loading
|
8
|
+
session.process_events_on { @image.loaded? }
|
9
|
+
end
|
10
|
+
|
11
|
+
subject { @image }
|
12
|
+
|
13
|
+
its(:status) { should be :ok }
|
14
|
+
its(:format) { should be :jpeg }
|
15
|
+
|
16
|
+
specify "its #id should match its’ spotify uri" do
|
17
|
+
@uri.should match @image.id
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#data" do
|
21
|
+
it "should correspond to the fixture image" do
|
22
|
+
@image.data.should eq File.read(fixture_image_path, :encoding => 'binary')
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have a binary encoding" do
|
26
|
+
@image.data.encoding.name.should eq 'ASCII-8BIT'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "callbacks" do
|
32
|
+
it "should trigger :load when loaded" do
|
33
|
+
uri = "spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663"
|
34
|
+
image = Hallon::Image.new(uri)
|
35
|
+
image.should_not be_loaded
|
36
|
+
image.should_receive(:trigger).with(:load).once
|
37
|
+
|
38
|
+
session.process_events_on { image.loaded? }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
describe Hallon::Link, :session => true do
|
2
|
+
subject { Hallon::Link.new("spotify:user:burgestrand") }
|
3
|
+
|
4
|
+
context "class methods" do
|
5
|
+
subject { described_class }
|
6
|
+
|
7
|
+
describe "::new" do
|
8
|
+
it "should raise an ArgumentError on an invalid link" do
|
9
|
+
expect { subject.new("omgwtfbbq") }.to raise_error(ArgumentError, /omgwtfbbq/)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should not raise error on valid links" do
|
13
|
+
expect { subject.new("spotify:user:burgestrand") }.to_not raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should accept an FFI pointer" do
|
17
|
+
expect { subject.new FFI::Pointer.new(0) }.to raise_error(ArgumentError, /is not a valid Spotify link/)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not initialize when given a Link" do
|
21
|
+
link = subject.new('spotify:user:burgestrand')
|
22
|
+
link.should_not_receive :to_str
|
23
|
+
subject.new link
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "::valid?" do
|
28
|
+
it "should be true for a valid link" do
|
29
|
+
subject.valid?("spotify:user:burgestrand").should be_true
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be false for an invalid link" do
|
33
|
+
subject.valid?("omgwtfbbq").should be_false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#to_str" do
|
39
|
+
it "should return the Spotify URI as a string" do
|
40
|
+
subject.to_str.should == "spotify:user:burgestrand"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should truncate if given a small maximum length" do
|
44
|
+
subject.to_str(7).should == "spotify"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#to_url" do
|
49
|
+
it "should return the correct http URL" do
|
50
|
+
subject.to_url.should == "http://open.spotify.com/user/burgestrand"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#length" do
|
55
|
+
it { subject.length.should == "spotify:user:burgestrand".length }
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#type" do
|
59
|
+
example_uris.each_pair do |uri, type|
|
60
|
+
specify "#{uri} should equal #{type}" do
|
61
|
+
Hallon::Link.new(uri).type.should equal type
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#to_s" do
|
67
|
+
it("should include the Spotify URI") do
|
68
|
+
subject.to_s.should include subject.to_str
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#==" do
|
73
|
+
it "should compare using #to_str" do
|
74
|
+
obj = Object.new
|
75
|
+
obj.should_receive(:to_str).and_return(subject.to_str)
|
76
|
+
|
77
|
+
subject.should eq obj
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should not fail when #to_str is unavailable" do
|
81
|
+
subject.should_not eq Object.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
describe Hallon::Linkable do
|
2
|
+
it "should define the #from_link method" do
|
3
|
+
klass = Class.new
|
4
|
+
klass.should_not respond_to :from_link
|
5
|
+
|
6
|
+
klass.instance_exec do
|
7
|
+
extend Hallon::Linkable
|
8
|
+
from_link :foobar
|
9
|
+
end
|
10
|
+
|
11
|
+
klass.should respond_to :from_link
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#from_link" do
|
15
|
+
it "should call the given block if necessary" do
|
16
|
+
called = false
|
17
|
+
klass = Class.new
|
18
|
+
|
19
|
+
klass.instance_exec do
|
20
|
+
extend Hallon::Linkable
|
21
|
+
from_link(nil) { called = true }
|
22
|
+
end
|
23
|
+
|
24
|
+
klass.from_link("spotify:search:whatever")
|
25
|
+
called.should eq true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should pass extra parameters to the defining block" do
|
29
|
+
klass = Class.new
|
30
|
+
|
31
|
+
link = mock
|
32
|
+
link.stub(:pointer)
|
33
|
+
Hallon::Link.stub(:new => link)
|
34
|
+
|
35
|
+
klass.instance_exec do
|
36
|
+
extend Hallon::Linkable
|
37
|
+
from_link(nil) { |link, *args| args }
|
38
|
+
end
|
39
|
+
|
40
|
+
klass.from_link("spotify:user:burgestrand", :cool, 5).should eq [:cool, 5]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
describe Hallon::Observable do
|
2
|
+
subject do
|
3
|
+
Class.new { include Hallon::Observable }.new
|
4
|
+
end
|
5
|
+
|
6
|
+
describe "instance methods" do
|
7
|
+
subject { described_class.instance_methods }
|
8
|
+
|
9
|
+
it { should include :on }
|
10
|
+
it { should include :trigger }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#on" do
|
14
|
+
it "should allow defining one handler for multiple events" do
|
15
|
+
subject.on(:a, :b, :c) do |event, *args|
|
16
|
+
"yay"
|
17
|
+
end
|
18
|
+
|
19
|
+
subject.trigger(:a).should eq "yay"
|
20
|
+
subject.trigger(:b).should eq "yay"
|
21
|
+
subject.trigger(:c).should eq "yay"
|
22
|
+
end
|
23
|
+
|
24
|
+
specify "a multi-declared handler should know its name" do
|
25
|
+
subject.on(:a, :b) { |event, *args| event }
|
26
|
+
subject.trigger(:a).should eq :a
|
27
|
+
subject.trigger(:b).should eq :b
|
28
|
+
end
|
29
|
+
|
30
|
+
specify "a single-declared handler should not know its name" do
|
31
|
+
subject.on(:a) { |event, *args| event }
|
32
|
+
subject.trigger(:a).should eq nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#trigger and #on" do
|
37
|
+
it "should define and call event handlers" do
|
38
|
+
called = false
|
39
|
+
subject.on(:a) { called = true }
|
40
|
+
subject.trigger(:a)
|
41
|
+
called.should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should pass any arguments to handlers" do
|
45
|
+
passed_args = []
|
46
|
+
subject.on(:a) { |*args| passed_args = args }
|
47
|
+
subject.trigger(:a, :b, :c)
|
48
|
+
passed_args.should eq [:b, :c]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should do nothing when there are no handlers" do
|
52
|
+
subject.trigger(:this_event_does_not_exist).should be_nil
|
53
|
+
end
|
54
|
+
|
55
|
+
context "multiple handlers" do
|
56
|
+
it "should call all handlers in order" do
|
57
|
+
triggered = []
|
58
|
+
subject.on(:a) { triggered << :a }
|
59
|
+
subject.on(:a) { triggered << :b }
|
60
|
+
subject.trigger(:a)
|
61
|
+
triggered.should eq [:a, :b]
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return the last-returned value" do
|
65
|
+
subject.on(:a) { :first }
|
66
|
+
subject.on(:a) { :second }
|
67
|
+
subject.trigger(:a).should eq :second
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should allow execution to be aborted" do
|
71
|
+
subject.on(:a) { throw :return, :first }
|
72
|
+
subject.on(:b) { :second }
|
73
|
+
subject.trigger(:a).should eq :first
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#protecting_handlers" do
|
79
|
+
it "should call the given block, returning the result" do
|
80
|
+
was_called = false
|
81
|
+
subject.protecting_handlers { was_called = true }.should be_true
|
82
|
+
was_called.should be_true
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should restore previous handlers on return" do
|
86
|
+
subject.on(:protected) { "before" }
|
87
|
+
|
88
|
+
subject.protecting_handlers do
|
89
|
+
subject.trigger(:protected).should eq "before"
|
90
|
+
subject.on(:protected) { "after" }
|
91
|
+
subject.trigger(:protected).should eq "after"
|
92
|
+
end
|
93
|
+
|
94
|
+
subject.trigger(:protected).should eq "before"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should still allow #trigger to work on non-defined events" do
|
98
|
+
subject.protecting_handlers {}
|
99
|
+
expect { subject.trigger(:does_not_exist) }.to_not raise_error
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|