Permalink
Browse files

Add feed streaming support

  • Loading branch information...
1 parent 9eb671f commit 3cfe7cc74b0e99a63c5f18493c00cf9163f33831 Gabriel Evans committed Dec 2, 2012
View
4 .pryrc
@@ -18,7 +18,9 @@ require 'eventful-ruby'
config_file = Pathname.new(Pathname.getwd.join('spec/config.yml'))
if config_file.exist?
- Eventful.api_key = YAML.load_file(config_file.to_s)['api_key']
+ config = YAML.load_file(config_file.to_s)
+ Eventful.api_key = config['api_key']
+ Eventful.feed_key = config['feed_key']
else
abort "Please setup a spec/config.yml file"
end
View
@@ -22,6 +22,9 @@ Gem::Specification.new do |gem|
gem.add_dependency 'multi_xml', '~> 0.5'
gem.add_dependency 'hashie', '~> 1.2.0'
+ # Feed streaming
+ gem.add_dependency 'em-http-request', '~> 1.0'
+
# Basic
gem.add_development_dependency 'rake'
gem.add_development_dependency 'bundler'
View
@@ -4,16 +4,17 @@
require 'eventful/exceptions'
module Eventful
- ENDPOINT = 'http://api.eventful.com/rest/'
+ API_ENDPOINT = 'http://api.eventful.com/rest/'
+ FEED_ENDPOINT = 'http://static.eventful.com/images/export/'
class << self
attr_accessor :api_key
-
+ attr_accessor :feed_key
end
-
end
require 'eventful/request'
require 'eventful/response'
require 'eventful/resource'
require 'eventful/event'
+require 'eventful/feeds'
View
@@ -1,6 +1,7 @@
module Eventful
- class Event < Resource
+ class Event
+ include Resource
class << self
@@ -94,6 +95,14 @@ def find(id, options={})
respond_with event, response, with_errors: true
end
+ def all(date = nil)
+ feed_for(:events, :full, date)
+ end
+
+ def updates(date = nil)
+ feed_for(:events, :updates, date)
+ end
+
end
end
@@ -0,0 +1,60 @@
+require 'nokogiri'
+
+module Eventful
+ module Feed
+ class Document < Nokogiri::XML::SAX::Document
+ attr_reader :resource_name
+
+ attr_reader :resource_class
+
+ def initialize(resource)
+ @resource_name = resource.to_s.singularize
+ @resource_class = "Eventful::#{resource_name.capitalize}".constantize
+ end
+
+ def start_element(name, attrs = [])
+ if name == resource_name || in_resource?
+ resource_stack << Node.new(name, Hash[*attrs.flatten])
+ end
+ end
+
+ def characters(string)
+ return unless in_resource?
+ resource_stack.last.add_node(string) unless string.strip.length == 0 || resource_stack.empty?
+ end
+ alias :cdata_block :characters
+
+ def end_element(name)
+ return unless in_resource?
+
+ if name == resource_name
+ last = resource_stack.pop
+ resources << build_resource(last)
+ resource_stack.clear
+ elsif resource_stack.size > 1
+ last = resource_stack.pop
+ resource_stack.last.add_node last
+ end
+ end
+
+ def in_resource?
+ !resource_stack.empty?
+ end
+
+ def resource_stack
+ @resource_stack ||= []
+ end
+
+ def resources
+ @resources ||= []
+ end
+
+ private
+
+ def build_resource(node)
+ data = node.to_hash[resource_name]
+ resource_class.instantiate(data)
+ end
+ end
+ end
+end
View
@@ -0,0 +1,201 @@
+require 'rexml/parsers/streamparser'
+require 'rexml/parsers/baseparser'
+require 'rexml/light/node'
+require 'rexml/text'
+require 'date'
+require 'time'
+require 'yaml'
+require 'bigdecimal'
+
+module Eventful
+ module Feed
+ ##
+ # Barely modified version of [Crack's REXMLUtilityNode](https://github.com/jnunemaker/crack/blob/master/lib/crack/xml.rb)
+ # which in turn is also a slightly modified version of the XMLUtilityNode from
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com). Open
+ # source wins again.
+ class Node
+ attr_accessor :name, :attributes, :children, :type
+
+ def self.typecasts
+ @@typecasts
+ end
+
+ def self.typecasts=(obj)
+ @@typecasts = obj
+ end
+
+ def self.available_typecasts
+ @@available_typecasts
+ end
+
+ def self.available_typecasts=(obj)
+ @@available_typecasts = obj
+ end
+
+ self.typecasts = {}
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
+ self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
+ self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
+ self.typecasts["string"] = lambda{|v| v.to_s}
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
+
+ self.available_typecasts = self.typecasts.keys
+
+ def initialize(name, normalized_attributes = {})
+
+ # unnormalize attribute values
+ attributes = Hash[* normalized_attributes.map { |key, value|
+ [ key, unnormalize_xml_entities(value) ]
+ }.flatten]
+
+ @name = name.tr("-", "_")
+ # leave the type alone if we don't know what it is
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
+
+ @nil_element = attributes.delete("nil") == "true"
+ @attributes = undasherize_keys(attributes)
+ @children = []
+ @text = false
+ end
+
+ def add_node(node)
+ @text = true if node.is_a? String
+ @children << node
+ end
+
+ def to_hash
+ if @type == "file"
+ f = StringIO.new((@children.first || '').unpack('m').first)
+ class << f
+ attr_accessor :original_filename, :content_type
+ end
+ f.original_filename = attributes['name'] || 'untitled'
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
+ return {name => f}
+ end
+
+ if @text
+ t = typecast_value( unnormalize_xml_entities( inner_html ) )
+ if t.is_a?(String)
+ class << t
+ attr_accessor :attributes
+ end
+ t.attributes = attributes
+ end
+ return { name => t }
+ else
+ #change repeating groups into an array
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
+
+ out = nil
+ if @type == "array"
+ out = []
+ groups.each do |k, v|
+ if v.size == 1
+ out << v.first.to_hash.entries.first.last
+ else
+ out << v.map{|e| e.to_hash[k]}
+ end
+ end
+ out = out.flatten
+
+ else # If Hash
+ out = {}
+ groups.each do |k,v|
+ if v.size == 1
+ out.merge!(v.first)
+ else
+ out.merge!( k => v.map{|e| e.to_hash[k]})
+ end
+ end
+ out.merge! attributes unless attributes.empty?
+ out = out.empty? ? nil : out
+ end
+
+ if @type && out.nil?
+ { name => typecast_value(out) }
+ else
+ { name => out }
+ end
+ end
+ end
+
+ # Typecasts a value based upon its type. For instance, if
+ # +node+ has #type == "integer",
+ # {{[node.typecast_value("12") #=> 12]}}
+ #
+ # @param value<String> The value that is being typecast.
+ #
+ # @details [:type options]
+ # "integer"::
+ # converts +value+ to an integer with #to_i
+ # "boolean"::
+ # checks whether +value+, after removing spaces, is the literal
+ # "true"
+ # "datetime"::
+ # Parses +value+ using Time.parse, and returns a UTC Time
+ # "date"::
+ # Parses +value+ using Date.parse
+ #
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
+ # The result of typecasting +value+.
+ #
+ # @note
+ # If +self+ does not have a "type" key, or if it's not one of the
+ # options specified above, the raw +value+ will be returned.
+ def typecast_value(value)
+ return value unless @type
+ proc = self.class.typecasts[@type]
+ proc.nil? ? value : proc.call(value)
+ end
+
+ # Take keys of the form foo-bar and convert them to foo_bar
+ def undasherize_keys(params)
+ params.keys.each do |key, value|
+ params[key.tr("-", "_")] = params.delete(key)
+ end
+ params
+ end
+
+ # Get the inner_html of the REXML node.
+ def inner_html
+ @children.join
+ end
+
+ # Converts the node into a readable HTML node.
+ #
+ # @return <String> The HTML node in text form.
+ def to_html
+ attributes.merge!(:type => @type ) if @type
+ "<#{name}#{to_xml_attributes(attributes)}>#{@nil_element ? '' : inner_html}</#{name}>"
+ end
+ alias :to_s :to_html
+
+ private
+
+ def snake_case(str)
+ return str.downcase if str =~ /^[A-Z]+$/
+ str.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
+ return $+.downcase
+ end
+
+ def to_xml_attributes(hash)
+ hash.map do |k,v|
+ %{#{snake_case(k.to_s).sub(/^(.{1,1})/) { |m| m.downcase }}="#{v.to_s.gsub('"', '&quot;')}"}
+ end.join(' ')
+ end
+
+ def unnormalize_xml_entities value
+ REXML::Text.unnormalize(value)
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 3cfe7cc

Please sign in to comment.