hallon 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.autotest +6 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +29 -0
  4. data/.rspec +7 -0
  5. data/.yardopts +8 -0
  6. data/CHANGELOG +20 -0
  7. data/Gemfile +2 -0
  8. data/LICENSE.txt +21 -0
  9. data/QUIRKS +11 -0
  10. data/README.markdown +58 -0
  11. data/Rakefile +75 -0
  12. data/Termfile +7 -0
  13. data/examples/logging_in.rb +26 -0
  14. data/examples/printing_link_information.rb +27 -0
  15. data/hallon.gemspec +31 -0
  16. data/lib/hallon.rb +34 -0
  17. data/lib/hallon/error.rb +54 -0
  18. data/lib/hallon/ext/ffi.rb +26 -0
  19. data/lib/hallon/ext/spotify.rb +101 -0
  20. data/lib/hallon/image.rb +70 -0
  21. data/lib/hallon/link.rb +101 -0
  22. data/lib/hallon/linkable.rb +50 -0
  23. data/lib/hallon/observable.rb +91 -0
  24. data/lib/hallon/session.rb +189 -0
  25. data/lib/hallon/synchronizable.rb +32 -0
  26. data/lib/hallon/user.rb +69 -0
  27. data/lib/hallon/version.rb +7 -0
  28. data/spec/fixtures/example_uris.rb +11 -0
  29. data/spec/fixtures/pink_cover.jpg +0 -0
  30. data/spec/hallon/error_spec.rb +30 -0
  31. data/spec/hallon/ffi_spec.rb +5 -0
  32. data/spec/hallon/hallon_spec.rb +16 -0
  33. data/spec/hallon/image_spec.rb +41 -0
  34. data/spec/hallon/link_spec.rb +84 -0
  35. data/spec/hallon/linkable_spec.rb +43 -0
  36. data/spec/hallon/observable_spec.rb +103 -0
  37. data/spec/hallon/session_spec.rb +61 -0
  38. data/spec/hallon/synchronizable_spec.rb +19 -0
  39. data/spec/hallon/user_spec.rb +73 -0
  40. data/spec/spec_helper.rb +71 -0
  41. data/spec/support/.gitkeep +0 -0
  42. data/spec/support/context_initialized_session.rb +3 -0
  43. data/spec/support/context_logged_in.rb +16 -0
  44. data/spec/support/cover_me.rb +5 -0
  45. data/spec/support/shared_for_loadable_objects.rb +7 -0
  46. 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
@@ -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,7 @@
1
+ # coding: utf-8
2
+ module Hallon
3
+ # Current release version of Hallon
4
+ #
5
+ # @see http://semver.org/
6
+ VERSION = [0, 1, 0].join('.')
7
+ 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
@@ -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,5 @@
1
+ describe FFI::Pointer do
2
+ it "should have a #read_size_t" do
3
+ described_class.instance_methods.should include :read_size_t
4
+ end
5
+ 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