Skip to content

Commit

Permalink
Add feed streaming support
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriel Evans committed Dec 2, 2012
1 parent 9eb671f commit 3cfe7cc
Show file tree
Hide file tree
Showing 14 changed files with 465 additions and 71 deletions.
4 changes: 3 additions & 1 deletion .pryrc
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions eventful-ruby.gemspec
Expand Up @@ -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'
Expand Down
7 changes: 4 additions & 3 deletions lib/eventful.rb
Expand Up @@ -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'
11 changes: 10 additions & 1 deletion lib/eventful/event.rb
@@ -1,6 +1,7 @@
module Eventful

class Event < Resource
class Event
include Resource

class << self

Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions lib/eventful/feed/document.rb
@@ -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
201 changes: 201 additions & 0 deletions lib/eventful/feed/node.rb
@@ -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

0 comments on commit 3cfe7cc

Please sign in to comment.