howkast 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ 0.0.0 [UNRELEASED]
2
+ - Initial release.
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "howkast"
16
+ gem.homepage = "http://github.com/jurisgalang/howkast"
17
+ gem.summary = %Q{Ruby bindings for the [Howcast](http://howcast.com) API}
18
+ gem.description = %Q{
19
+ This gem implements Ruby bindings for the [Howcast](http://howcast.com) API
20
+ Howcast empowers people with engaging, useful how-to information wherever, whenever they need to know how.
21
+ }
22
+ gem.email = "jurisgalang@gmail.com"
23
+ gem.authors = ["Juris Galang"]
24
+ gem.license = ["MIT", "GPL"]
25
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
26
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
27
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
28
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
29
+ end
30
+ Jeweler::RubygemsDotOrgTasks.new
31
+
32
+ require 'rspec/core'
33
+ require 'rspec/core/rake_task'
34
+ RSpec::Core::RakeTask.new(:spec) do |spec|
35
+ spec.pattern = FileList['spec/**/*_spec.rb']
36
+ end
37
+
38
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
39
+ spec.pattern = 'spec/**/*_spec.rb'
40
+ spec.rcov = true
41
+ end
42
+
43
+ require 'cucumber/rake/task'
44
+ Cucumber::Rake::Task.new(:features)
45
+
46
+ task :default => :spec
47
+
48
+ require 'yard'
49
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'howkast'
12
+
13
+ require 'rspec/expectations'
@@ -0,0 +1,109 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{howkast}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Juris Galang"]
12
+ s.date = %q{2011-01-03}
13
+ s.description = %q{
14
+ This gem implements Ruby bindings for the [Howcast](http://howcast.com) API
15
+ Howcast empowers people with engaging, useful how-to information wherever, whenever they need to know how.
16
+ }
17
+ s.email = %q{jurisgalang@gmail.com}
18
+ s.extra_rdoc_files = [
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ "GPL-LICENSE",
25
+ "Gemfile",
26
+ "Gemfile.lock",
27
+ "MIT-LICENSE",
28
+ "README.md",
29
+ "RELEASENOTES",
30
+ "Rakefile",
31
+ "VERSION",
32
+ "features/howkast.feature",
33
+ "features/step_definitions/howkast_steps.rb",
34
+ "features/support/env.rb",
35
+ "howkast.gemspec",
36
+ "lib/howkast.rb",
37
+ "lib/howkast/api.rb",
38
+ "lib/howkast/base.rb",
39
+ "lib/howkast/configuration.rb",
40
+ "lib/howkast/errors.rb",
41
+ "lib/howkast/ext/icebox.rb",
42
+ "lib/howkast/ext/string.rb",
43
+ "lib/howkast/model.rb",
44
+ "lib/howkast/processors/base.rb",
45
+ "lib/howkast/processors/categories.rb",
46
+ "lib/howkast/processors/playlists.rb",
47
+ "lib/howkast/processors/search.rb",
48
+ "lib/howkast/processors/users.rb",
49
+ "lib/howkast/processors/videos.rb",
50
+ "sample/adhoc.rb",
51
+ "sample/quickstart.rb",
52
+ "spec/howkast/api_service_spec.rb",
53
+ "spec/howkast/api_spec.rb",
54
+ "spec/howkast/model_spec.rb",
55
+ "spec/howkast/processors/categories_spec.rb",
56
+ "spec/howkast/processors/playlists_spec.rb",
57
+ "spec/howkast/processors/search_spec.rb",
58
+ "spec/howkast/processors/users_spec.rb",
59
+ "spec/howkast/processors/videos_spec.rb",
60
+ "spec/howkast_spec.rb",
61
+ "spec/spec_helper.rb"
62
+ ]
63
+ s.homepage = %q{http://github.com/jurisgalang/howkast}
64
+ s.licenses = [["MIT", "GPL"]]
65
+ s.require_paths = ["lib"]
66
+ s.rubygems_version = %q{1.3.7}
67
+ s.summary = %q{Ruby bindings for the [Howcast](http://howcast.com) API}
68
+ s.test_files = [
69
+ "spec/howkast/api_service_spec.rb",
70
+ "spec/howkast/api_spec.rb",
71
+ "spec/howkast/model_spec.rb",
72
+ "spec/howkast/processors/categories_spec.rb",
73
+ "spec/howkast/processors/playlists_spec.rb",
74
+ "spec/howkast/processors/search_spec.rb",
75
+ "spec/howkast/processors/users_spec.rb",
76
+ "spec/howkast/processors/videos_spec.rb",
77
+ "spec/howkast_spec.rb",
78
+ "spec/spec_helper.rb"
79
+ ]
80
+
81
+ if s.respond_to? :specification_version then
82
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
83
+ s.specification_version = 3
84
+
85
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
86
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
87
+ s.add_development_dependency(%q<yard>, ["~> 0.6.0"])
88
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
89
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
90
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
91
+ s.add_development_dependency(%q<rcov>, [">= 0"])
92
+ else
93
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
94
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
95
+ s.add_dependency(%q<cucumber>, [">= 0"])
96
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
97
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
98
+ s.add_dependency(%q<rcov>, [">= 0"])
99
+ end
100
+ else
101
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
102
+ s.add_dependency(%q<yard>, ["~> 0.6.0"])
103
+ s.add_dependency(%q<cucumber>, [">= 0"])
104
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
105
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
106
+ s.add_dependency(%q<rcov>, [">= 0"])
107
+ end
108
+ end
109
+
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ require 'time'
6
+ require 'singleton'
7
+ require 'active_support/memoizable'
8
+ require 'httparty'
9
+ require 'named-parameters'
10
+
11
+ require 'howkast/ext/string'
12
+ #require 'howkast/ext/icebox'
13
+
14
+ require 'howkast/configuration'
15
+ require 'howkast/errors'
16
+ require 'howkast/model'
17
+ require 'howkast/base'
18
+ require 'howkast/api'
19
+
20
+ Dir[File.join(File.dirname(__FILE__), 'howkast', 'processors', '*')].each do |processor|
21
+ require processor
22
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ class API
5
+ service :video,
6
+ processor: :videos,
7
+ options: { :required => :id }
8
+
9
+ service :videos,
10
+ options: { :required => [ :sort, :filter ],
11
+ :optional => [ :category, :page ] }
12
+
13
+ service :search,
14
+ options: { :required => :query,
15
+ :optional => { :view => :video } }
16
+
17
+ service :user,
18
+ processor: :users,
19
+ options: { :required => [ :id, :resource ] }
20
+
21
+ service :playlist,
22
+ processor: :playlists,
23
+ options: { :required => :id }
24
+
25
+ service :category,
26
+ processor: :categories,
27
+ options: { :required => :id }
28
+
29
+ service :categories
30
+
31
+ recognizes :api_key
32
+ def initialize opts = { }
33
+ key = opts[:api_key]
34
+ configuration.api_key = key if key
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ def self.configure config = { }
5
+ configuration = Configuration.instance
6
+ config.each{ |k, v| configuration.send "#{k}=", v }
7
+ end
8
+
9
+ module Base
10
+ HOWCAST_BASE_URI = 'www.howcast.com'
11
+ extend ActiveSupport::Memoizable
12
+
13
+ def self.included base
14
+ base.send :include, HTTParty
15
+ #base.send :include, HTTParty::Icebox
16
+
17
+ base.base_uri HOWCAST_BASE_URI
18
+ base.format :xml
19
+ #base.cache :store => Configuration.instance.cache_store,
20
+ # :timeout => Configuration.instance.cache_timeout,
21
+ # :location => Configuration.instance.cache_location
22
+
23
+ base.extend ClassMethods
24
+ end
25
+
26
+ def configuration
27
+ Configuration.instance
28
+ end
29
+
30
+ private
31
+ def request *args
32
+ r = self.class.get *args
33
+ raise Howkast::Error::RequestError(r.code, r.parsed_response['howcast']) \
34
+ unless r.code == 200
35
+ r.parsed_response['howcast']
36
+ end
37
+ memoize :request
38
+
39
+ module ClassMethods
40
+ def configuration
41
+ Configuration.instance
42
+ end
43
+
44
+ def services
45
+ @services
46
+ end
47
+
48
+ def service name, spec = { }
49
+ arguments_guard = ->(argcount) do
50
+ maxarity = spec[:maxarity] || 1
51
+ raise ArgumentError, "wrong number of arguments (#{argcount} for #{maxarity})" \
52
+ unless maxarity == argcount
53
+ end
54
+
55
+ has_named_parameters name, spec[:options] || { }
56
+ (@services ||= []) << name.to_sym
57
+
58
+ define_method name do |*args, &block|
59
+ arguments_guard[args.count]
60
+
61
+ self.class.default_params api_key: configuration.api_key
62
+ procname = (spec[:processor] || name).to_s.modulize
63
+ processor = Processor.const_defined?(procname) ?
64
+ Processor.const_get(procname) :
65
+ Processor::Base
66
+
67
+ path = "/#{processor.path || name}"
68
+ query = args.last
69
+ args = args[0..-2] if query.instance_of? Hash
70
+ query = { } unless query.instance_of? Hash
71
+
72
+ processor.filter args, query
73
+
74
+ path << "/#{args.join('/')}" unless args.empty?
75
+ path << '.xml'
76
+
77
+ data = request(path, query: query)
78
+ processor.parse_element data
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ class API
85
+ include Base
86
+ end
87
+
88
+ module Processor
89
+ end
90
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ class Configuration
5
+ include Singleton
6
+ attr_accessor :api_key
7
+ attr_accessor :cache_timeout
8
+ attr_accessor :cache_location
9
+ attr_accessor :cache_store
10
+
11
+ def initialize
12
+ @cache_timeout = 600
13
+ @cache_location = '/tmp/howkast/cache'
14
+ @cache_store = 'file'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Howkast
4
+ module Error
5
+ class RequestError < StandardError; end
6
+
7
+ def self.RequestError(code, data)
8
+ msg = data['response']['err']['msg'] unless data.nil?
9
+ const = msg ? "#{msg.gsub(/\w+/){ $&.modulize }.gsub(/ /, '')}" : "HTTP#{code}"
10
+ klass = if Howkast::Error.const_defined? const
11
+ Howkast::Error.const_get const
12
+ else
13
+ Howkast::Error.const_set const, Class.new(RequestError)
14
+ end
15
+ klass.new(msg)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,261 @@
1
+ # encoding: utf-8
2
+ #
3
+ # See: https://gist.github.com/209605
4
+ #
5
+ # = Icebox : Caching for HTTParty
6
+ #
7
+ # Cache responses in HTTParty models [http://github.com/jnunemaker/httparty]
8
+ #
9
+ # === Usage
10
+ #
11
+ # class Foo
12
+ # include HTTParty
13
+ # include HTTParty::Icebox
14
+ # cache :store => 'file', :timeout => 600, :location => MY_APP_ROOT.join('tmp', 'cache')
15
+ # end
16
+ #
17
+ # Modeled after Martyn Loughran's APICache [http://github.com/newbamboo/api_cache]
18
+ # and Ruby On Rails's caching [http://api.rubyonrails.org/classes/ActiveSupport/Cache.html]
19
+ #
20
+ # Author: Karel Minarik [www.karmi.cz]
21
+ #
22
+ # === Notes
23
+ #
24
+ # Thanks to Amit Chakradeo to point out objects have to be stored marhalled on FS
25
+ # Thanks to Marlin Forbes to point out query parameters have to be include in the cache key
26
+ #
27
+ require 'logger'
28
+ #require 'ftools'
29
+ require 'tmpdir'
30
+ require 'pathname'
31
+ require 'digest/md5'
32
+
33
+ module HTTParty #:nodoc:
34
+ module Icebox
35
+
36
+ module ClassMethods
37
+
38
+ # Enable caching and set cache options
39
+ # Returns memoized cache object
40
+ #
41
+ # Following options are available, default values are in []:
42
+ #
43
+ # +store+:: Storage mechanism for cached data (memory, filesystem, your own) [memory]
44
+ # +timeout+:: Cache expiration in seconds [60]
45
+ # +logger+:: Path to logfile or logger instance [STDOUT]
46
+ #
47
+ # Any additional options are passed to the Cache constructor
48
+ #
49
+ # Usage:
50
+ #
51
+ # # Enable caching in HTTParty, in memory, for 1 minute
52
+ # cache # Use default values
53
+ #
54
+ # # Enable caching in HTTParty, on filesystem (/tmp), for 10 minutes
55
+ # cache :store => 'file', :timeout => 600, :location => '/tmp/'
56
+ #
57
+ # # Use your own cache store (see AbstractStore class below)
58
+ # cache :store => 'memcached', :timeout => 600, :server => '192.168.1.1:1001'
59
+ #
60
+ def cache(options={})
61
+ options[:store] ||= 'memory'
62
+ options[:timeout] ||= 60
63
+ logger = options[:logger]
64
+ @cache ||= Cache.new( options.delete(:store), options )
65
+ end
66
+
67
+ end
68
+
69
+ # When included, extend class with +cache+ method
70
+ # and redefine +get+ method to use cache
71
+ #
72
+ def self.included(receiver) #:nodoc:
73
+ receiver.extend ClassMethods
74
+ receiver.class_eval do
75
+
76
+ # Get reponse from network
77
+ # TODO: Why alias :new :old is not working here? Returns NoMethodError
78
+ #
79
+ def self.get_without_caching(path, options={})
80
+ perform_request Net::HTTP::Get, path, options
81
+ end
82
+
83
+ # Get response from cache, if available
84
+ #
85
+ def self.get_with_caching(path, options={})
86
+ key = path.clone
87
+ key << options[:query].to_s if defined? options[:query]
88
+
89
+ if cache.exists?(key) and not cache.stale?(key)
90
+ Cache.logger.debug "CACHE -- GET #{path}#{options[:query]}"
91
+ return cache.get(key)
92
+ else
93
+ Cache.logger.debug "/!\\ NETWORK -- GET #{path}#{options[:query]}"
94
+ response = get_without_caching(path, options)
95
+ cache.set(key, response) if response.code == 200
96
+ return response
97
+ end
98
+ end
99
+
100
+ # Redefine original HTTParty +get+ method to use cache
101
+ #
102
+ def self.get(path, options={})
103
+ self.get_with_caching(path, options)
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ # === Cache container
110
+ #
111
+ # Pass a store name ('memory', etc) to initializer
112
+ #
113
+ class Cache
114
+ attr_accessor :store
115
+
116
+ def initialize(store, options={})
117
+ self.class.logger = options[:logger]
118
+ @store = self.class.lookup_store(store).new(options)
119
+ end
120
+
121
+ def get(key); @store.get encode(key) unless stale?(key); end
122
+ def set(key, value); @store.set encode(key), value; end
123
+ def exists?(key); @store.exists? encode(key); end
124
+ def stale?(key); @store.stale? encode(key); end
125
+
126
+ def self.logger; @logger || default_logger; end
127
+ def self.default_logger; logger = ::Logger.new(STDERR); end
128
+
129
+ # Pass a filename (String), IO object, Logger instance or +nil+ to silence the logger
130
+ def self.logger=(device); @logger = device.kind_of?(::Logger) ? device : ::Logger.new(device); end
131
+
132
+ private
133
+
134
+ # Return store class based on passed name
135
+ def self.lookup_store(name)
136
+ store_name = "#{name.capitalize}Store"
137
+ return Store::const_get(store_name)
138
+ rescue NameError => e
139
+ raise Store::StoreNotFound, "The cache store '#{store_name}' was not found. Did you loaded any such class?"
140
+ end
141
+
142
+ def encode(key); Digest::MD5.hexdigest(key); end
143
+ end
144
+
145
+
146
+ # === Cache stores
147
+ #
148
+ module Store
149
+
150
+ class StoreNotFound < StandardError; end #:nodoc:
151
+
152
+ # ==== Abstract Store
153
+ # Inherit your store from this class
154
+ # *IMPORTANT*: Do not forget to call +super+ in your +initialize+ method!
155
+ #
156
+ class AbstractStore
157
+ def initialize(options={})
158
+ raise ArgumentError, "You need to set the :timeout parameter" unless options[:timeout]
159
+ @timeout = options[:timeout]
160
+ message = "Cache: Using #{self.class.to_s.split('::').last}"
161
+ message << " in location: #{options[:location]}" if options[:location]
162
+ message << " with timeout #{options[:timeout]} sec"
163
+ Cache.logger.info message unless options[:logger].nil?
164
+ return self
165
+ end
166
+ %w{set get exists? stale?}.each do |method_name|
167
+ define_method(method_name) { raise NoMethodError, "Please implement method set in your store class" }
168
+ end
169
+ end
170
+
171
+ # ===== Store objects in memory
172
+ #
173
+ Struct.new("Response", :code, :body, :headers) { def to_s; self.body; end }
174
+ class MemoryStore < AbstractStore
175
+ def initialize(options={})
176
+ super; @store = {}; self
177
+ end
178
+ def set(key, value)
179
+ Cache.logger.info("Cache: set (#{key})")
180
+ @store[key] = [Time.now, value]; true
181
+ end
182
+ def get(key)
183
+ data = @store[key][1]
184
+ Cache.logger.info("Cache: #{data.nil? ? "miss" : "hit"} (#{key})")
185
+ data
186
+ end
187
+ def exists?(key)
188
+ !@store[key].nil?
189
+ end
190
+ def stale?(key)
191
+ return true unless exists?(key)
192
+ Time.now - created(key) > @timeout
193
+ end
194
+ private
195
+ def created(key)
196
+ @store[key][0]
197
+ end
198
+ end
199
+
200
+ # ===== Store objects on the filesystem
201
+ #
202
+ class FileStore < AbstractStore
203
+ def initialize(options={})
204
+ super
205
+ options[:location] ||= Dir::tmpdir
206
+ @path = Pathname.new( options[:location] )
207
+ FileUtils.mkdir_p( @path )
208
+ self
209
+ end
210
+ def set(key, value)
211
+ Cache.logger.info("Cache: set (#{key})")
212
+ File.open( @path.join(key), 'w' ) { |file| file << Marshal.dump(value) }
213
+ true
214
+ end
215
+ def get(key)
216
+ data = Marshal.load(File.read( @path.join(key)))
217
+ Cache.logger.info("Cache: #{data.nil? ? "miss" : "hit"} (#{key})")
218
+ data
219
+ end
220
+ def exists?(key)
221
+ File.exists?( @path.join(key) )
222
+ end
223
+ def stale?(key)
224
+ return true unless exists?(key)
225
+ Time.now - created(key) > @timeout
226
+ end
227
+ private
228
+ def created(key)
229
+ File.mtime( @path.join(key) )
230
+ end
231
+ end
232
+ end
233
+
234
+ end
235
+ end
236
+ #
237
+ # Major parts of this code are based on architecture of ApiCache.
238
+ # Copyright (c) 2008 Martyn Loughran
239
+ #
240
+ # Other parts are inspired by the ActiveSupport::Cache in Ruby On Rails.
241
+ # Copyright (c) 2005-2009 David Heinemeier Hansson
242
+ #
243
+ # Permission is hereby granted, free of charge, to any person obtaining
244
+ # a copy of this software and associated documentation files (the
245
+ # "Software"), to deal in the Software without restriction, including
246
+ # without limitation the rights to use, copy, modify, merge, publish,
247
+ # distribute, sublicense, and/or sell copies of the Software, and to
248
+ # permit persons to whom the Software is furnished to do so, subject to
249
+ # the following conditions:
250
+ #
251
+ # The above copyright notice and this permission notice shall be
252
+ # included in all copies or substantial portions of the Software.
253
+ #
254
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
255
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
256
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
257
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
258
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
259
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
260
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
261
+ #