diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..777d8eb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +rvm: + - "1.9.3" + - jruby-19mode + - rbx-19mode + - jruby-18mode + - "1.8.7" diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index b3fdf08..ee32937 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,3 +1,11 @@ +# 1.0.0.beta.1 + +- All slot interactions now go through `Ducksboard::Slot` + +Breaking Changes: + +- Breaks anything depending on slot type specific classes + # 0.1.5 - #6 Fixed push requests diff --git a/README.mdown b/README.mdown index 2b28632..1501c01 100644 --- a/README.mdown +++ b/README.mdown @@ -1,84 +1,51 @@ ## Ducksboard API Ruby wrapper +[![Code Climate](https://codeclimate.com/github/jhsu/ducksboard.png)](https://codeclimate.com/github/jhsu/ducksboard) +[![Build Status](https://travis-ci.org/jhsu/ducksboard.png?branch=simple-slot-push)](https://travis-ci.org/jhsu/ducksboard) + ### Configuration -API Key can be set in the environment +API Key can be set in the environment (such as `~/.bashrc`). export DUCKSBOARD_API_KEY='YOURKEY' -or in an initializer +or in an initializer (such as `config/initializers/ducksboard.rb`) Ducksboard.api_key = 'YOURKEY' -### Box - - widget = Ducksboard::Box.new(1234) # Widget numeric id - widget.value = 10 - widget.save - -### Counter - - widget = Ducksboard::Counter.new(1234) - widget.value = 10 - widget.save - -### Image - - widget = Ducksboard::Image.new(1235) - widget.source = "https://dashboard.ducksboard.com/static/accounts/img/logo_small.png" - # or - widget.source = "~/Pictures/logo.png" - widget.caption = "Ducksboard logo!" - widget.timestamp = 1310649204 - widget.save - -### Gauge - - widget = Ducksboard::Gauge.new(1235) - widget.value = 0.93 - widget.save - -### Graph - - # remember that the graph widgets need atleast 2 points before it displays anything - widget = Ducksboard::Graph.new(1236) - widget.timestamp = Time.now.to_i - widget.value = 198 - widget.save +### Sending Data (Push API) -### Pin +Data can be sent to slots in formats specified in the [Ducksboard API +documentation](http://dev.ducksboard.com/apidoc/slot-kinds). - widget = Ducksboard::Pin.new(1234) - widget.value = 10 - widget.save +```ruby +# Update the value a slot (ie. counter) +slot = Ducksboard::Slot.new(123) +slot.update(:value => 20) -### Timeline +# Update a leaderboard +slot = Ducksboard::Slot.new("leaderboard") +slot.update(:value => { + :boards => [ + {:name => "person 1", values => [123, 24.5]}, + {:name => "person 2", values => [224, 21.0]} + ] +}) +``` - widget = Ducksboard::Timeline.new(1237) - widget.title = "A Title" - widget.image = "http://url.to.io/some_image.gif" - # or - widget.image = :edited - # any of the following as a string or symbol: orange, red, green, created, edited or deleted - widget.content = "text content" - widget.link = "http://google.com" +### Pulling Data (Pull API) -### Leaderboard +Fetching data from a slot on Ducksboard. Several convinience methods are +provided to fetch data through the [Ducksboard HTTP Pull +API](http://dev.ducksboard.com/apidoc/pull-api-http/#resource-endpoints). Each +pull returns a hash of data with a data at `response['data']`. - widget1 = Ducksboard::Leaderboard.new(56803) - widget1.linha = [{"name" => 'Titulo 1', "values" => [12,13,14]}, - {"name" => 'Titulo 2', "values" => [12,13,142]}, - ] - widget1.save +```ruby +slot = Ducksboard::Slot.new(123) +slot.last(:count => 15) -### Pull API +slot.since(:seconds => 5 * 60) - # You can use the HTTP Pull API to retrieve historical data from any widget. - # It will be returned in a map as per the API (http://dev.ducksboard.com/apidoc/pull-api-http) +slot.timespan(:timespan => "weekly", :timezone => "UTC") +``` - widget = Ducksboard::Gauge.new(1235) - data = widget.since(300)['data'] - # or - data = widget.last_values(15) - # or - data = widget.timespan(:daily, "Europe/London") diff --git a/Rakefile b/Rakefile index 93f8aa3..100a001 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,7 @@ require 'rake/testtask' Rake::TestTask.new do |t| t.libs << "test" << "lib" - t.test_files = FileList['test/*_test.rb'] + t.test_files = FileList['test/**/*_test.rb'] end task :default => :test diff --git a/ducksboard.gemspec b/ducksboard.gemspec index 2919a94..277a7c5 100644 --- a/ducksboard.gemspec +++ b/ducksboard.gemspec @@ -1,6 +1,8 @@ +require File.join(File.dirname(__FILE__), 'lib/ducksboard/version') + Gem::Specification.new do |s| s.name = 'ducksboard' - s.version = '0.1.5' + s.version = Ducksboard::VERSION s.summary = "API wrapper for ducksboard.com dashboard" s.description = "Ruby API wrapper for ducksboard realtime dashboard using HTTParty" s.authors = ["Joseph Hsu"] @@ -10,9 +12,14 @@ Gem::Specification.new do |s| s.license = 'MIT' s.required_ruby_version = '>= 1.8.7' - s.add_runtime_dependency 'httparty', '~> 0.8', '>= 0.8.1' + s.add_runtime_dependency 'addressable', '~> 2.3', '>= 2.3.3' + s.add_runtime_dependency 'httparty', '~> 0.10', '>= 0.10.2' + s.add_runtime_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_development_dependency 'rake', '~> 10.0' s.add_development_dependency 'minitest', '~> 4.6.2' + s.add_development_dependency 'webmock', '~> 1.11', '>= 1.11.0' + s.add_development_dependency 'mocha', '~> 0.13', '>= 0.13.3' + s.add_development_dependency 'pry', '~> 0.9', '>= 0.9.2' s.post_install_message = <<-DESC Quack! (in real-time)" diff --git a/lib/ducksboard.rb b/lib/ducksboard.rb index b1660cf..d902963 100644 --- a/lib/ducksboard.rb +++ b/lib/ducksboard.rb @@ -1,4 +1,5 @@ require 'httparty' +require "addressable/uri" module Ducksboard class << self ; attr_accessor :api_key end @@ -7,12 +8,8 @@ def self.api_key end end -require 'ducksboard/widget' -require 'ducksboard/box' -require 'ducksboard/counter' -require 'ducksboard/gauge' -require 'ducksboard/graph' -require 'ducksboard/image' -require 'ducksboard/pin' -require 'ducksboard/timeline' -require 'ducksboard/leaderboard' +require 'ducksboard/version' +require 'ducksboard/request' +require 'ducksboard/push' +require 'ducksboard/pull' +require 'ducksboard/slot' diff --git a/lib/ducksboard/box.rb b/lib/ducksboard/box.rb deleted file mode 100644 index f7a5531..0000000 --- a/lib/ducksboard/box.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Ducksboard - class Box < Widget - def valid? - @data[:value].is_a?(Integer) - end - end -end - diff --git a/lib/ducksboard/counter.rb b/lib/ducksboard/counter.rb deleted file mode 100644 index e6cb132..0000000 --- a/lib/ducksboard/counter.rb +++ /dev/null @@ -1,4 +0,0 @@ -module Ducksboard - class Counter < Widget - end -end diff --git a/lib/ducksboard/gauge.rb b/lib/ducksboard/gauge.rb deleted file mode 100644 index 0bf0945..0000000 --- a/lib/ducksboard/gauge.rb +++ /dev/null @@ -1,4 +0,0 @@ -module Ducksboard - class Gauge < Widget - end -end diff --git a/lib/ducksboard/graph.rb b/lib/ducksboard/graph.rb deleted file mode 100644 index 9b92c9c..0000000 --- a/lib/ducksboard/graph.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ducksboard - class Graph < Widget - def valid? - @data[:timestamp] && @data[:value] - end - end -end diff --git a/lib/ducksboard/image.rb b/lib/ducksboard/image.rb deleted file mode 100644 index fc7e2d5..0000000 --- a/lib/ducksboard/image.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'base64' -require 'json' -module Ducksboard - class Image < Widget - - def initialize(*args) - super - @data[:value] ||={} - end - - def source - @data[:value][:source] - end - - def source=(image_location) - source_value = if image_location =~ /^http/ - image_location - else - 'data:image/png;base64,' + - Base64.encode64(File.read(File.expand_path(image_location))) - end - @data[:value][:source] = source_value - end - - def caption - @data[:value][:caption] - end - - def caption=(text=nil) - @data[:value][:caption] = caption.to_s - end - - def timestamp - @data[:timestamp] - end - - def timestamp=(time) - @data[:timestamp] = time - end - - end -end diff --git a/lib/ducksboard/leaderboard.rb b/lib/ducksboard/leaderboard.rb deleted file mode 100644 index f62f9c6..0000000 --- a/lib/ducksboard/leaderboard.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'base64' -require 'json' -module Ducksboard - class Leaderboard < Widget - - def initialize(*args) - super - @data[:value] ||={} - end - - def linha - @data[:value][:board] - end - - def linha=(linhaa) - @data[:value][:board] = linhaa - end - - end -end diff --git a/lib/ducksboard/pin.rb b/lib/ducksboard/pin.rb deleted file mode 100644 index acfe943..0000000 --- a/lib/ducksboard/pin.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Ducksboard - class Pin < Widget - def valid? - @data[:value].is_a?(Integer) - end - end -end diff --git a/lib/ducksboard/pull.rb b/lib/ducksboard/pull.rb new file mode 100644 index 0000000..b6a38df --- /dev/null +++ b/lib/ducksboard/pull.rb @@ -0,0 +1,49 @@ +module Ducksboard + module Pull + include ::HTTParty + include Request + PULL_URL = "https://pull.ducksboard.com/values" + + # Get the last :count values. + # + # options - A hash of options: + # count - A integer of the last :count values to fetch. + # + # Returns a hash response. + def last(options={}) + get("last", options) + end + + # Get data since timestamp. + # + # options - A hash of options: + # seconds - A start time in secods. + # + # Returns a hash response. + def since(options={}) + get("since", options) + end + + # Get data summarized by timespan. + # + # options - A hash of options: + # timespan - A string timespan. + # timezone - A string timezone (default: UTC). + # + # Returns a hash response. + def timespan(options={}) + get("timespan", options) + end + + protected + + # Internal: Get request for data. + # + # endpoint - string. + def get(endpoint, params={}) + uri = Addressable::URI.new + uri.query_values = params + HTTParty.get("#{PULL_URL}/#{id}/#{endpoint}?#{uri.query}", :basic_auth => auth) + end + end +end diff --git a/lib/ducksboard/push.rb b/lib/ducksboard/push.rb new file mode 100644 index 0000000..0c61575 --- /dev/null +++ b/lib/ducksboard/push.rb @@ -0,0 +1,59 @@ +module Ducksboard + module Push + include Request + PUSH_URL = "https://push.ducksboard.com/values" + + # Update slot with data. + # + # data - A hash or array of hashes of data to send with the update: + # value - A new value for a slot. + # delta - A change in value for a slot. + # timestamp - A time or unix timestamp(optional). + # + # Returns nothing. + def update(data={}) + data[:timestamp] = time_to_unix(data[:timestamp]) if data[:timestamp] + post(data) + end + + # Deletes all data for a given data source. + # + # Returns nothing. + def destroy + delete + end + + protected + + # Internal: Convert time to unix timestamp if needed. + # + # Returns a fixnum. + def time_to_unix(time) + if time.respond_to?(:to_i) + time.to_i + else + time + end + end + + # Internal: Sends http request. + # + # sending_data - A hash converted #to_json before sent (required). + # + # Returns nothing. + def post(sending_data) + self.class.post( + "#{PUSH_URL}/#{id}", + :basic_auth => auth, + :body => sending_data.to_json + ) + end + + # Internal: Send delete request. + # + # Returns nothing. + def delete + self.class.delete("#{PUSH_URL}/#{id}", :basic_auth => auth) + end + end +end diff --git a/lib/ducksboard/request.rb b/lib/ducksboard/request.rb new file mode 100644 index 0000000..db17ba0 --- /dev/null +++ b/lib/ducksboard/request.rb @@ -0,0 +1,14 @@ +module Ducksboard + module Request + include ::HTTParty + + protected + + # Internal: Basic auth hash. + # + # Returns a hash. + def auth + {:username => Ducksboard.api_key, :password => "ducksboard-gem"} + end + end +end diff --git a/lib/ducksboard/slot.rb b/lib/ducksboard/slot.rb new file mode 100644 index 0000000..09e4f20 --- /dev/null +++ b/lib/ducksboard/slot.rb @@ -0,0 +1,15 @@ +module Ducksboard + class Slot + include Push + include Pull + + attr_accessor :id + + # Initialize a new slot. + # + # id - A slot . + def initialize(id) + @id = id + end + end +end diff --git a/lib/ducksboard/timeline.rb b/lib/ducksboard/timeline.rb deleted file mode 100644 index 2622b1c..0000000 --- a/lib/ducksboard/timeline.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Ducksboard - class Timeline < Widget - - ICONS = { - :orange => "https://app.ducksboard.com/static/img/timeline/orange.gif", - :red => "https://app.ducksboard.com/static/img/timeline/red.gif", - :green => "https://app.ducksboard.com/static/img/timeline/green.gif", - :created => "https://app.ducksboard.com/static/img/timeline/created.png", - :edited => "https://app.ducksboard.com/static/img/timeline/edited.png", - :deleted => "https://app.ducksboard.com/static/img/timeline/deleted.png" - } - - def initialize(*args) - super - @data[:value] ||={} - end - - def title; @data[:title] end - def image; @data[:image] end - def content; @data[:image] end - def link; @data[:image] end - - def title=(text) - @data[:value][:title] = text - end - - def image=(url) - @data[:value][:image] = if url =~ /^http/ - url - else - ICONS[url.to_sym] - end - end - - def content=(text) - @data[:value][:content] = text - end - - def link=(url) - @data[:value][:link] = url - end - end -end diff --git a/lib/ducksboard/version.rb b/lib/ducksboard/version.rb new file mode 100644 index 0000000..6bdb329 --- /dev/null +++ b/lib/ducksboard/version.rb @@ -0,0 +1,4 @@ +module Ducksboard + VERSION = Version = "1.0.0.beta.1" +end + diff --git a/lib/ducksboard/widget.rb b/lib/ducksboard/widget.rb deleted file mode 100644 index 4a2b602..0000000 --- a/lib/ducksboard/widget.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'json' -module Ducksboard - class Widget - include ::HTTParty - - attr_accessor :id, :data, :type - - def initialize(id, data={}) - @id = id - @data = data - end - - def value - @data[:value] - end - - def value=(val) - @data[:value] = val - end - - def timestamp - @data[:timestamp] - end - - def timestamp=(time) - @data[:timestamp] = time - end - - def update(data=nil) - @data = data if data - self.class.post("#{PUSH_URI}/#{@id.to_s}", - :basic_auth => auth, - :body => @data.to_json) - end - - def save - if valid? - update.code.to_i == 200 - else - raise "Invalid Data: #{@data.inspect}" - end - end - - def valid? - true - end - - def last_values(number_of_values=3) - pull("last?count=#{number_of_values}") - end - - def since(seconds_ago=3600) - pull("since?seconds=#{seconds_ago.to_i}") - end - - def timespan(timespan=:monthly, timezone="UTC") - pull("timespan?timespan=#{timespan.to_s}&timezone=#{timezone}") - end - - private - - PUSH_URI = "https://push.ducksboard.com/values" - PULL_URI = "https://pull.ducksboard.com/values" - - def auth() - {:username => ::Ducksboard.api_key, :password => "ducksboard-gem"} - end - - def pull(service_uri) - response = self.class.get("#{PULL_URI}/#{@id.to_s}/#{service_uri}", - :basic_auth => auth) - - if response.code.to_i == 200 - JSON.parse(response.body) - else - raise "Unexpected response code: #{response.code}; body: #{response.body}" - end - end - end -end diff --git a/test/ducksboard/slot_test.rb b/test/ducksboard/slot_test.rb new file mode 100644 index 0000000..dc14c8c --- /dev/null +++ b/test/ducksboard/slot_test.rb @@ -0,0 +1,132 @@ +require 'minitest_helper' + +describe Ducksboard::Slot do + let(:slot) { Ducksboard::Slot.new(123) } + + it "can initialize with an id" do + slot.id.must_equal 123 + end + + + describe "Push" do + it "sends data" do + data = {:value => 123 } + + slot.expects(:post).with(data) + slot.update(data) + end + + it "ensures timestamp is unix timestamp" do + time = Time.now + unix_time = time.to_i + + slot.expects(:post).with({:timestamp => unix_time}) + slot.update({:timestamp => time}) + end + + it "can be destroyed" do + slot.expects(:delete) + slot.destroy + end + end # Push + + describe "Pull" do + it "grabs last number of data" do + stub_request(:get, /pull\.ducksboard\.com.*last/).to_return( + :body => <<-RESPONSE, +{ + "count": 15, + "data": [ + { + "timestamp": 1332971928.20006, + "value": 132.0 + } + ] +} + RESPONSE + :headers => {'Content-Type' => 'application/json'}) + response = slot.last() + response.must_be_kind_of Hash + end + + it "grabs data since" do + stub_request(:get, /pull\.ducksboard.com.*since/).to_return( + :body => <<-RESPONSE, +{ + "count": 15, + "data": [ + { + "timestamp": 1332971928.20006, + "value": 132.0 + } + ] +} + RESPONSE + :headers => {'Content-Type' => 'application/json'}) + response = slot.since() + response.must_be_kind_of Hash + end + + it "grabs data by timespan" do + stub_request(:get, /pull\.ducksboard.com.*timespan/).to_return( + :body => <<-RESPONSE, +{ + "count": 9, + "data": [ + { + "timestamp": null, + "period": null, + "value": null + }, + { + "timestamp": 1355875200.0, + "period": 3, + "value": null + }, + { + "timestamp": 1355961600.0, + "period": 4, + "value": null + }, + { + "timestamp": 1356048000.0, + "period": 5, + "value": 1.0 + }, + { + "timestamp": 1356134400.0, + "period": 6, + "value": 2.0 + }, + { + "timestamp": 1356220800.0, + "period": 7, + "value": null + }, + { + "timestamp": 1356307200.0, + "period": 1, + "value": 3.0 + }, + { + "timestamp": 1356393600.0, + "period": 2, + "value": null + }, + { + "timestamp": 1356480000.0, + "period": 3, + "value": null + } + ] +} + RESPONSE + :headers => {'Content-Type' => 'application/json'}) + + response = slot.timespan(:timespan => "weekly", :timezone => "UTC") + response.must_be_kind_of(Hash) + end + + end # Pull + +end diff --git a/test/minitest_helper.rb b/test/minitest_helper.rb index ea09eae..b5d773a 100644 --- a/test/minitest_helper.rb +++ b/test/minitest_helper.rb @@ -1,4 +1,10 @@ +require 'rubygems' +require 'bundler/setup' + require 'minitest/spec' require 'minitest/autorun' +require 'mocha' +require 'webmock/minitest' require 'ducksboard' +require 'pry'