hallon 0.0.0 → 0.1.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/.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
|