benschwarz-smoke 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +16 -6
- data/VERSION.yml +1 -1
- data/lib/smoke/origin.rb +62 -29
- data/lib/smoke/request.rb +1 -1
- data/lib/smoke/source/data.rb +0 -14
- data/lib/smoke/source/yql.rb +24 -5
- data/lib/smoke.rb +1 -1
- data/spec/smoke/origin_spec.rb +37 -16
- data/spec/smoke/source/feed_spec.rb +17 -11
- data/spec/smoke/source/yql_spec.rb +45 -13
- data/spec/spec.opts +1 -1
- data/spec/supports/test_source.rb +1 -3
- metadata +2 -2
data/README.markdown
CHANGED
@@ -4,7 +4,11 @@ smoke is a Ruby based DSL that allows you to take data from YQL, RSS / Atom (and
|
|
4
4
|
This "data" can then be re-represented, sorted and filtered. You can collect data from a multiude of sources, sort them on a common property
|
5
5
|
and return a plain old ruby object or json (You could add in something to output XML too)
|
6
6
|
|
7
|
-
|
7
|
+
## Media
|
8
|
+
|
9
|
+
* [Presentation from Melbourne #roro](http://www.slideshare.net/benschwarz/smoke-1371124)
|
10
|
+
* Early [screencast](http://vimeo.com/4272804) to get developer / peer feedback
|
11
|
+
|
8
12
|
|
9
13
|
## The concept
|
10
14
|
|
@@ -13,9 +17,8 @@ The concept comes from using [Yahoo Pipes](http://pipes.yahoo.com) to make littl
|
|
13
17
|
## How or what to contribute
|
14
18
|
|
15
19
|
* Test everything you do
|
16
|
-
* Add a source (I was thinking a source that took simply took a url and parsed whatever it got)
|
17
20
|
* Add a way to output (XML, anyone?)
|
18
|
-
* Examples of queries you'd like to be able to do
|
21
|
+
* Examples of queries you'd like to be able to do (email / github message them to me)
|
19
22
|
|
20
23
|
## API Examples
|
21
24
|
### YQL
|
@@ -53,13 +56,20 @@ Integrity [is running for smoke](http://integrity.ffolio.net/smoke)
|
|
53
56
|
|
54
57
|
### TODO (working on, just mental notes)
|
55
58
|
|
56
|
-
*
|
59
|
+
* Clean up / simplify source activation process
|
60
|
+
* Consider variable injection (eg, allow to query sending a username within some aspect of the request)
|
61
|
+
* Implement a disaptch / request method that will actually thread property (event-machine)
|
62
|
+
* Checkout experimental fakeweb version to stub out the yql specs
|
63
|
+
|
64
|
+
#### Later / maybe
|
57
65
|
* Allow for sources to explicitly set the content type being returned for those stupid content providers
|
58
|
-
* Consider invokation methods (registering of sources, namespacing et al)
|
59
|
-
* YQL Definitions
|
60
66
|
* YQL w/oAuth
|
61
67
|
* YQL Subqueries?
|
62
68
|
|
69
|
+
#### For wiki pages (docs, later)
|
70
|
+
* How to use `path`
|
71
|
+
* YQL Definitions
|
72
|
+
|
63
73
|
### Copyright
|
64
74
|
|
65
75
|
Copyright (c) 2009 Ben Schwarz. See LICENSE for details.
|
data/VERSION.yml
CHANGED
data/lib/smoke/origin.rb
CHANGED
@@ -5,10 +5,60 @@ module Smoke
|
|
5
5
|
def initialize(name, &block)
|
6
6
|
raise StandardError, "Sources must have a name" unless name
|
7
7
|
@name = name
|
8
|
-
@items = []
|
8
|
+
@items, @transformation = [], []
|
9
|
+
|
9
10
|
activate!
|
10
11
|
instance_eval(&block) if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Output your items in a range of formats (:ruby, :json and :yaml currently)
|
15
|
+
# Ruby is the default format and will automagically yielded from your source
|
16
|
+
#
|
17
|
+
# Usage
|
18
|
+
#
|
19
|
+
# output(:json)
|
20
|
+
# => "[{title: \"Ray\"}, {title: \"Peace\"}]"
|
21
|
+
def output(type = :ruby)
|
11
22
|
dispatch if respond_to? :dispatch
|
23
|
+
output = (@items.length == 1) ? @items.first : @items
|
24
|
+
|
25
|
+
case type
|
26
|
+
when :ruby
|
27
|
+
return output
|
28
|
+
when :json
|
29
|
+
return ::JSON.generate(output)
|
30
|
+
when :yaml
|
31
|
+
return YAML.dump(output)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def items=(items) # :nodoc:
|
36
|
+
@items = items.flatten.map{|i| i.symbolize_keys! }
|
37
|
+
invoke_transformation
|
38
|
+
end
|
39
|
+
|
40
|
+
# Path allows you to traverse the tree of a the items returned to
|
41
|
+
# only give you access to what you're interested in.
|
42
|
+
#
|
43
|
+
# Usage:
|
44
|
+
# path :down, :to, :the, :data
|
45
|
+
#
|
46
|
+
# Will traverse through a tree as follows:
|
47
|
+
#
|
48
|
+
# {
|
49
|
+
# :down => {
|
50
|
+
# :to => {
|
51
|
+
# :the => {
|
52
|
+
# :data => []
|
53
|
+
# }
|
54
|
+
# }
|
55
|
+
# }
|
56
|
+
# }
|
57
|
+
#
|
58
|
+
# You will need to help smoke find an array of
|
59
|
+
# items that you're interested in.
|
60
|
+
def path(*path)
|
61
|
+
@path = path
|
12
62
|
end
|
13
63
|
|
14
64
|
# Transform each item
|
@@ -18,7 +68,7 @@ module Smoke
|
|
18
68
|
# rename(:href => :link)
|
19
69
|
# end
|
20
70
|
def emit(&block)
|
21
|
-
|
71
|
+
@transformation << DelayedBlock.new(&block)
|
22
72
|
end
|
23
73
|
|
24
74
|
# Re-sort items by a particular key
|
@@ -43,7 +93,7 @@ module Smoke
|
|
43
93
|
@items.reject! {|i| (i[key] =~ matcher) ? false : true }
|
44
94
|
return self
|
45
95
|
end
|
46
|
-
|
96
|
+
|
47
97
|
# Discard items that do not match the regex
|
48
98
|
#
|
49
99
|
# Usage (chained):
|
@@ -57,7 +107,7 @@ module Smoke
|
|
57
107
|
@items.reject! {|i| (i[key] =~ matcher) ? true : false }
|
58
108
|
return self
|
59
109
|
end
|
60
|
-
|
110
|
+
|
61
111
|
# Rename one or many keys at a time
|
62
112
|
# Suggested that you run it within an each block, but it makes no difference
|
63
113
|
# other than readability
|
@@ -71,25 +121,7 @@ module Smoke
|
|
71
121
|
@items.each {|item| item.rename(*args) }
|
72
122
|
return self
|
73
123
|
end
|
74
|
-
|
75
|
-
# Output your items in a range of formats (:ruby, :json and :yaml currently)
|
76
|
-
# Ruby is the default format and will automagically yielded from your source
|
77
|
-
#
|
78
|
-
# Usage
|
79
|
-
#
|
80
|
-
# output(:json)
|
81
|
-
# => "[{title: \"Ray\"}, {title: \"Peace\"}]"
|
82
|
-
def output(type = :ruby)
|
83
|
-
case type
|
84
|
-
when :ruby
|
85
|
-
return @items
|
86
|
-
when :json
|
87
|
-
return ::JSON.generate(@items)
|
88
|
-
when :yaml
|
89
|
-
return YAML.dump(@items)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
124
|
+
|
93
125
|
# Truncate your result set to this many objects
|
94
126
|
#
|
95
127
|
# Usage
|
@@ -101,14 +133,15 @@ module Smoke
|
|
101
133
|
return self
|
102
134
|
end
|
103
135
|
|
104
|
-
|
105
|
-
|
106
|
-
|
136
|
+
private
|
137
|
+
def invoke_transformation
|
138
|
+
@transformation.each{|t| t.execute! } unless @transformation.nil?
|
107
139
|
end
|
108
140
|
|
109
|
-
|
110
|
-
|
111
|
-
|
141
|
+
def drill(*path)
|
142
|
+
path.inject(nil) do |obj, pointer|
|
143
|
+
obj = obj.nil? ? pointer : obj[pointer]
|
144
|
+
end
|
112
145
|
end
|
113
146
|
|
114
147
|
def activate!
|
data/lib/smoke/request.rb
CHANGED
data/lib/smoke/source/data.rb
CHANGED
@@ -32,25 +32,11 @@ module Smoke
|
|
32
32
|
@url = source_url
|
33
33
|
end
|
34
34
|
|
35
|
-
# Path allows you to traverse the tree of a the items returned to
|
36
|
-
# only give you access to what you're interested in. In the case
|
37
|
-
# of the comments in this document I traverse to the photos returned.
|
38
|
-
def path(*path)
|
39
|
-
@path = path
|
40
|
-
end
|
41
|
-
|
42
35
|
protected
|
43
36
|
def dispatch
|
44
37
|
@request = Smoke::Request.new(@url)
|
45
38
|
self.items = (@path.nil?) ? @request.body : drill(@request.body, *@path)
|
46
39
|
end
|
47
|
-
|
48
|
-
private
|
49
|
-
def drill(*path)
|
50
|
-
path.inject(nil) do |obj, pointer|
|
51
|
-
obj = obj.nil? ? pointer : obj[pointer]
|
52
|
-
end
|
53
|
-
end
|
54
40
|
end
|
55
41
|
end
|
56
42
|
end
|
data/lib/smoke/source/yql.rb
CHANGED
@@ -16,6 +16,7 @@ module Smoke
|
|
16
16
|
# where :query, "ruby"
|
17
17
|
# end
|
18
18
|
class YQL < Origin
|
19
|
+
API_BASE = "http://query.yahooapis.com/v1/public/yql"
|
19
20
|
attr_reader :request
|
20
21
|
|
21
22
|
# Select indicates what YQL will be selecting
|
@@ -26,7 +27,7 @@ module Smoke
|
|
26
27
|
# => "SELECT title"
|
27
28
|
# select :title, :description
|
28
29
|
# => "SELECT title, description"
|
29
|
-
def select(what)
|
30
|
+
def select(what = :all)
|
30
31
|
@select = what.join(",") and return if what.is_a? Array
|
31
32
|
@select = "*" and return if what == :all
|
32
33
|
@select = what.to_s
|
@@ -53,20 +54,38 @@ module Smoke
|
|
53
54
|
@where << "#{column.to_s} = '#{value}'"
|
54
55
|
end
|
55
56
|
|
57
|
+
# `use` can be used to set the url location of the data-table
|
58
|
+
# that you want YQL to search upon
|
59
|
+
#
|
60
|
+
# Usage:
|
61
|
+
# use "http://datatables.org/alltables.env"
|
62
|
+
def use(url)
|
63
|
+
params.merge!({:env => url})
|
64
|
+
end
|
65
|
+
|
56
66
|
protected
|
67
|
+
def params
|
68
|
+
@params || @params = {}
|
69
|
+
end
|
70
|
+
|
57
71
|
def dispatch
|
58
72
|
@request = Smoke::Request.new(build_uri)
|
59
|
-
self.items = @request.body
|
73
|
+
self.items = [(@path.nil?) ? @request.body : drill(@request.body, *@path)]
|
60
74
|
end
|
61
75
|
|
62
76
|
private
|
63
|
-
|
64
77
|
def build_uri
|
65
|
-
|
78
|
+
chunks = []
|
79
|
+
default_opts = {
|
80
|
+
:q => build_query,
|
81
|
+
:format => "json"
|
82
|
+
}.merge(params).each {|k,v| chunks << "#{k}=#{v}" }
|
83
|
+
|
84
|
+
return URI.encode(API_BASE + "?" + chunks.join("&"))
|
66
85
|
end
|
67
86
|
|
68
87
|
def build_query
|
69
|
-
|
88
|
+
"SELECT #{@select} FROM #{@from} WHERE #{@where.join(" AND ")}"
|
70
89
|
end
|
71
90
|
end
|
72
91
|
end
|
data/lib/smoke.rb
CHANGED
data/spec/smoke/origin_spec.rb
CHANGED
@@ -18,6 +18,10 @@ describe Smoke::Origin do
|
|
18
18
|
it "should be ordered by title" do
|
19
19
|
@origin.output.first[:title].should == "Kangaroo"
|
20
20
|
end
|
21
|
+
|
22
|
+
it "should output a single hash rather than a hash in an array when there is one item" do
|
23
|
+
Smoke[:test].truncate(1).output.should == {:title => "Kangaroo"}
|
24
|
+
end
|
21
25
|
end
|
22
26
|
|
23
27
|
describe "transformations" do
|
@@ -44,7 +48,7 @@ describe Smoke::Origin do
|
|
44
48
|
end
|
45
49
|
|
46
50
|
it "should only contain items that match" do
|
47
|
-
Smoke[:keep].keep(:head, /^K/).output.should ==
|
51
|
+
Smoke[:keep].keep(:head, /^K/).output.should == {:head => "Kangaroo"}
|
48
52
|
end
|
49
53
|
|
50
54
|
it "should discard items" do
|
@@ -52,25 +56,36 @@ describe Smoke::Origin do
|
|
52
56
|
end
|
53
57
|
|
54
58
|
it "should not contain items that match" do
|
55
|
-
Smoke[:discard].discard(:head, /^K/).output.should ==
|
59
|
+
Smoke[:discard].discard(:head, /^K/).output.should == {:head => "Platypus"}
|
56
60
|
end
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
64
|
+
describe "output" do
|
65
|
+
it "should output" do
|
66
|
+
@origin.output.should be_an_instance_of(Array)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should output two items" do
|
70
|
+
@origin.output.size.should == 2
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should output json" do
|
74
|
+
@origin.output(:json).should =~ /^\[\{/
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should output yml" do
|
78
|
+
@origin.output(:yaml).should =~ /--- \n- :title:/
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should dispatch when output is called" do
|
82
|
+
TestSource.source(:no_dispatch)
|
83
|
+
Smoke[:no_dispatch].should_not_receive(:dispatch)
|
84
|
+
|
85
|
+
TestSource.source(:dispatch)
|
86
|
+
Smoke[:dispatch].should_receive(:dispatch)
|
87
|
+
Smoke[:dispatch].output
|
88
|
+
end
|
74
89
|
end
|
75
90
|
|
76
91
|
it "method chaining" do
|
@@ -81,4 +96,10 @@ describe Smoke::Origin do
|
|
81
96
|
it "should softly error when attempting to sort on a key that doesn't exist" do
|
82
97
|
TestSource.source(:chain).sort(:header).should_not raise_error("NoMethodError")
|
83
98
|
end
|
99
|
+
|
100
|
+
describe "variable injection" do
|
101
|
+
it "should allow variables to be left happlessly in your source definitions"
|
102
|
+
it "should allow setting of variables"
|
103
|
+
it "should error if output is called before a variable has been set"
|
104
|
+
end
|
84
105
|
end
|
@@ -27,16 +27,22 @@ describe "Feed" do
|
|
27
27
|
Smoke[:slashdot].should respond_to(:url)
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
30
|
+
describe "after dispatch / query" do
|
31
|
+
before do
|
32
|
+
Smoke[:slashdot].output
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should accept multiple urls" do
|
36
|
+
Smoke[:slashdot].requests.should be_an_instance_of(Array)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should hold the url used to query" do
|
40
|
+
Smoke[:slashdot].requests.collect{|r| r.uri }.should include("http://slashdot.org/index.rdf")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should have renamed url to link" do
|
44
|
+
Smoke[:slashdot].output.first.should have_key(:url)
|
45
|
+
Smoke[:slashdot].output.first.should_not have_key(:link)
|
46
|
+
end
|
41
47
|
end
|
42
48
|
end
|
@@ -4,15 +4,13 @@ describe "YQL" do
|
|
4
4
|
before :all do
|
5
5
|
# Fake web does not yet support regex matched uris
|
6
6
|
|
7
|
-
|
8
|
-
# response.body = File.read(File.join(SPEC_DIR, 'supports', 'search-web.yql'))
|
9
|
-
# response.content_type "text/json"
|
10
|
-
#end
|
7
|
+
FakeWeb.register_uri("query.yahooapis.com/*", :response => File.join(SPEC_DIR, 'supports', 'search-web.yql'))
|
11
8
|
|
12
9
|
Smoke.yql(:search) do
|
13
10
|
select :all
|
14
11
|
from "search.web"
|
15
12
|
where :query, "ruby"
|
13
|
+
path :query, :results, :result
|
16
14
|
|
17
15
|
emit do
|
18
16
|
rename(:url => :link)
|
@@ -28,16 +26,50 @@ describe "YQL" do
|
|
28
26
|
Smoke[:search].items.should be_an_instance_of(Array)
|
29
27
|
end
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
describe "after dispatch" do
|
30
|
+
before do
|
31
|
+
Smoke[:search].output
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should hold the url used to query" do
|
35
|
+
Smoke[:search].request.uri.should == "http://query.yahooapis.com/v1/public/yql?q=SELECT%20*%20FROM%20search.web%20WHERE%20query%20=%20'ruby'&format=json"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have renamed url to link" do
|
39
|
+
Smoke[:search].output.first.should have_key(:link)
|
40
|
+
Smoke[:search].output.first.should_not have_key(:url)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should output a ruby object" do
|
44
|
+
Smoke[:search].output.should be_an_instance_of(Array)
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
|
-
|
41
|
-
|
48
|
+
describe "yql definitions" do
|
49
|
+
before do
|
50
|
+
Smoke.yql(:smoke) do
|
51
|
+
use "http://datatables.org/alltables.env"
|
52
|
+
|
53
|
+
select :all
|
54
|
+
from "github.repo"
|
55
|
+
where :id, "benschwarz"
|
56
|
+
where :repo, "smoke"
|
57
|
+
path :query, :results
|
58
|
+
end
|
59
|
+
|
60
|
+
Smoke[:smoke].output # Force execution
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should be a respository" do
|
64
|
+
Smoke[:smoke].output.should have_key(:repository)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should respond to use" do
|
68
|
+
Smoke[:smoke].should respond_to(:use)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should contain 'env' within the query string" do
|
72
|
+
Smoke[:smoke].request.uri.should =~ /env=/
|
73
|
+
end
|
42
74
|
end
|
43
75
|
end
|
data/spec/spec.opts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
-c
|
1
|
+
-c
|
@@ -1,8 +1,6 @@
|
|
1
1
|
module TestSource
|
2
2
|
def self.source(name, &block)
|
3
|
-
source = Smoke::Origin.
|
4
|
-
source.stub!(:dispatch)
|
5
|
-
source.send(:initialize, name, &block || Proc.new {})
|
3
|
+
source = Smoke::Origin.new(name, &block || Proc.new {})
|
6
4
|
source.items = [
|
7
5
|
{:head => "Platypus"},
|
8
6
|
{:head => "Kangaroo"}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: benschwarz-smoke
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Schwarz
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-05-31 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|