hotwire 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +82 -0
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/hotwire.gemspec +30 -23
- data/lib/hotwire.rb +13 -7
- data/lib/hotwire/base.rb +44 -0
- data/lib/hotwire/request.rb +60 -0
- data/lib/hotwire/response.rb +18 -0
- data/lib/hotwire/response/base.rb +54 -0
- data/lib/hotwire/response/csv.rb +17 -0
- data/lib/hotwire/response/html.rb +11 -0
- data/lib/hotwire/response/invalid.rb +16 -0
- data/lib/hotwire/response/json.rb +96 -0
- data/test/response/test_base.rb +46 -0
- data/test/response/test_csv.rb +27 -0
- data/test/response/test_html.rb +26 -0
- data/test/response/test_invalid.rb +24 -0
- data/test/response/test_json.rb +30 -0
- data/test/test_active_record_mixins.rb +61 -65
- data/test/test_helper.rb +12 -0
- data/test/test_request.rb +56 -0
- data/test/test_response.rb +42 -0
- metadata +31 -24
- data/README.makrdown +0 -46
- data/lib/hotwire/active_record_mixins.rb +0 -95
- data/lib/hotwire/column_headers.rb +0 -54
- data/lib/hotwire/core_extensions.rb +0 -11
- data/lib/hotwire/row.rb +0 -46
- data/lib/hotwire/table.rb +0 -27
- data/test/active_record_test_helper.rb +0 -14
- data/test/test_column_headers.rb +0 -20
- data/test/test_core_extensions.rb +0 -26
- data/test/test_row.rb +0 -17
- data/test/test_table.rb +0 -40
data/README.markdown
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Hotwire
|
2
|
+
|
3
|
+
Hotwire is designed to ease the pain of creating Google Wire Protocol compatible data source in Ruby. Specifically, Hotwire can make it dead simple to provide data for the Google Visualization API.
|
4
|
+
|
5
|
+
This class implements a Google Visualization API Wire Protocal Datasource, as defined [here](http://code.google.com/intl/it/apis/visualization/documentation/dev/implementing_data_source.html).
|
6
|
+
|
7
|
+
You can use it to wrap your custom data into a json, csv, or html output compliant with the Google Visualization APIs specifications.
|
8
|
+
|
9
|
+
Google Visualization APIs define a standard format to express tabular data with typed columns. By exposing your data in such format, you can use all the existing visualizations conforming to the specification out-of-the-box to represent and analyze your data.
|
10
|
+
|
11
|
+
You can read more about the GViz API here: http://code.google.com/apis/visualization.
|
12
|
+
|
13
|
+
It implements most of the standard with the exclusion of:
|
14
|
+
* Query language (tq parameters)
|
15
|
+
* Caching (sig parameters)
|
16
|
+
* some warnings and error types in the JSON response
|
17
|
+
|
18
|
+
It supports version 0.5 of the APIs.
|
19
|
+
|
20
|
+
## Basic Use
|
21
|
+
The basic use follows this pattern:
|
22
|
+
|
23
|
+
* create an instance from the request params using the Hotwire::Request.from_paramsfactory method.
|
24
|
+
* verify that the instance is valid
|
25
|
+
* create a response object using the request.build_response method
|
26
|
+
* add columns to the response using add_column.
|
27
|
+
* add a dataset to the response using set_data.
|
28
|
+
* invoke the body method to return the datasource formatted according to the Wire Protocol in the requested format.
|
29
|
+
|
30
|
+
Depending on the request parameters, the body method will either return a json or csv string. When an html response is requested, nothing is returned by this class itself can be passed to a view to generate the HTML report.
|
31
|
+
|
32
|
+
The dataset is expected to be a 2-dimensional array (the first index referencing the rows, the second the columns). More generally, it can be anything that supports a 2 nested iteration cycles: the first on rows, the second on columns.
|
33
|
+
|
34
|
+
The columns ordering in the dataset must match the order used to add them via the add_column method. Aka, add_column must be invoked for each column in the same order used by the dataset (and expected in the produced output).
|
35
|
+
|
36
|
+
## Example
|
37
|
+
The following is a basic example of use.
|
38
|
+
|
39
|
+
class YourController < ApplicationController
|
40
|
+
def yourmethod
|
41
|
+
data = [
|
42
|
+
[ Date.today, 'hello', 10 ],
|
43
|
+
[ Date.today - 1, 'world', 20 ]
|
44
|
+
]
|
45
|
+
respond_to do |wants|
|
46
|
+
wants.json do
|
47
|
+
if wire_request = Hotwire::Request.from_param(params)
|
48
|
+
wire_response = wire_request.build_response
|
49
|
+
wire_response.add_col('date', :id => 'A' , :label => 'Date').
|
50
|
+
add_col('string', :id => 'B' , :label => 'Name').
|
51
|
+
add_col('number', :id => 'C', :label => 'Count').
|
52
|
+
set_data(data) if wire_req.valid?
|
53
|
+
wire_request.body
|
54
|
+
else
|
55
|
+
data.to_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
## ToDo
|
62
|
+
TODO: should be updated to handle version 0.6 of the API
|
63
|
+
|
64
|
+
## Author ##
|
65
|
+
Les Kiger [les@codebenders.com]
|
66
|
+
|
67
|
+
## Acknowledgements
|
68
|
+
This code is heavily based on the GVis class by Riccardo Govoni [battlehorse@gmail.com].
|
69
|
+
|
70
|
+
## Note on Patches/Pull Requests
|
71
|
+
|
72
|
+
* Fork the project.
|
73
|
+
* Make your feature addition or bug fix.
|
74
|
+
* Add tests for it. This is important so I don't break it in a
|
75
|
+
future version unintentionally.
|
76
|
+
* Commit, do not mess with rakefile, version, or history.
|
77
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
78
|
+
* Send me a pull request. Bonus points for topic branches.
|
79
|
+
|
80
|
+
## Copyright
|
81
|
+
|
82
|
+
Copyright (c) 2010 Les Freeman. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -10,9 +10,10 @@ begin
|
|
10
10
|
gem.email = "les@codebenders.com"
|
11
11
|
gem.homepage = "http://github.com/lesfreeman/hotwire"
|
12
12
|
gem.authors = ["Les Freeman"]
|
13
|
-
gem.add_dependency "activesupport", ">= 0"
|
13
|
+
# gem.add_dependency "activesupport", ">= 0"
|
14
14
|
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
15
15
|
gem.add_development_dependency "redgreen", ">= 0"
|
16
|
+
gem.add_development_dependency "mocha", ">= 0"
|
16
17
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
18
|
end
|
18
19
|
Jeweler::GemcutterTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.0
|
data/hotwire.gemspec
CHANGED
@@ -5,39 +5,44 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{hotwire}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Les Freeman"]
|
12
|
-
s.date = %q{2010-10-
|
12
|
+
s.date = %q{2010-10-25}
|
13
13
|
s.description = %q{Hotwire is designed to ease the pain of creating Google Wire protocol compatible data source in Ruby.}
|
14
14
|
s.email = %q{les@codebenders.com}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
17
|
-
"README.
|
17
|
+
"README.markdown"
|
18
18
|
]
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
21
|
".gitignore",
|
22
22
|
"LICENSE",
|
23
|
-
"README.
|
23
|
+
"README.markdown",
|
24
24
|
"Rakefile",
|
25
25
|
"VERSION",
|
26
26
|
"hotwire.gemspec",
|
27
27
|
"lib/hotwire.rb",
|
28
|
-
"lib/hotwire/
|
29
|
-
"lib/hotwire/
|
30
|
-
"lib/hotwire/
|
31
|
-
"lib/hotwire/
|
32
|
-
"lib/hotwire/
|
33
|
-
"
|
28
|
+
"lib/hotwire/base.rb",
|
29
|
+
"lib/hotwire/request.rb",
|
30
|
+
"lib/hotwire/response.rb",
|
31
|
+
"lib/hotwire/response/base.rb",
|
32
|
+
"lib/hotwire/response/csv.rb",
|
33
|
+
"lib/hotwire/response/html.rb",
|
34
|
+
"lib/hotwire/response/invalid.rb",
|
35
|
+
"lib/hotwire/response/json.rb",
|
34
36
|
"test/hotwire_test.rb",
|
37
|
+
"test/response/test_base.rb",
|
38
|
+
"test/response/test_csv.rb",
|
39
|
+
"test/response/test_html.rb",
|
40
|
+
"test/response/test_invalid.rb",
|
41
|
+
"test/response/test_json.rb",
|
35
42
|
"test/test_active_record_mixins.rb",
|
36
|
-
"test/test_column_headers.rb",
|
37
|
-
"test/test_core_extensions.rb",
|
38
43
|
"test/test_helper.rb",
|
39
|
-
"test/
|
40
|
-
"test/
|
44
|
+
"test/test_request.rb",
|
45
|
+
"test/test_response.rb"
|
41
46
|
]
|
42
47
|
s.homepage = %q{http://github.com/lesfreeman/hotwire}
|
43
48
|
s.rdoc_options = ["--charset=UTF-8"]
|
@@ -45,14 +50,16 @@ Gem::Specification.new do |s|
|
|
45
50
|
s.rubygems_version = %q{1.3.6}
|
46
51
|
s.summary = %q{Under the hood data transformations for the Google Wire protocol.}
|
47
52
|
s.test_files = [
|
48
|
-
"test/
|
49
|
-
"test/
|
53
|
+
"test/hotwire_test.rb",
|
54
|
+
"test/response/test_base.rb",
|
55
|
+
"test/response/test_csv.rb",
|
56
|
+
"test/response/test_html.rb",
|
57
|
+
"test/response/test_invalid.rb",
|
58
|
+
"test/response/test_json.rb",
|
50
59
|
"test/test_active_record_mixins.rb",
|
51
|
-
"test/test_column_headers.rb",
|
52
|
-
"test/test_core_extensions.rb",
|
53
60
|
"test/test_helper.rb",
|
54
|
-
"test/
|
55
|
-
"test/
|
61
|
+
"test/test_request.rb",
|
62
|
+
"test/test_response.rb"
|
56
63
|
]
|
57
64
|
|
58
65
|
if s.respond_to? :specification_version then
|
@@ -60,18 +67,18 @@ Gem::Specification.new do |s|
|
|
60
67
|
s.specification_version = 3
|
61
68
|
|
62
69
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
63
|
-
s.add_runtime_dependency(%q<activesupport>, [">= 0"])
|
64
70
|
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
65
71
|
s.add_development_dependency(%q<redgreen>, [">= 0"])
|
72
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
66
73
|
else
|
67
|
-
s.add_dependency(%q<activesupport>, [">= 0"])
|
68
74
|
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
69
75
|
s.add_dependency(%q<redgreen>, [">= 0"])
|
76
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
70
77
|
end
|
71
78
|
else
|
72
|
-
s.add_dependency(%q<activesupport>, [">= 0"])
|
73
79
|
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
74
80
|
s.add_dependency(%q<redgreen>, [">= 0"])
|
81
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
75
82
|
end
|
76
83
|
end
|
77
84
|
|
data/lib/hotwire.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Hotwire
|
2
|
+
def self.supported_api_versions
|
3
|
+
["0.5"]
|
4
|
+
end
|
5
|
+
end
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
require 'hotwire/base'
|
8
|
+
require 'hotwire/request'
|
9
|
+
require 'hotwire/response'
|
10
|
+
require 'hotwire/response/base'
|
11
|
+
require 'hotwire/response/csv'
|
12
|
+
require 'hotwire/response/json'
|
13
|
+
require 'hotwire/response/html'
|
14
|
+
require 'hotwire/response/invalid'
|
data/lib/hotwire/base.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module Hotwire
|
5
|
+
class Base
|
6
|
+
@@error_reasons = Set.new([
|
7
|
+
:not_modified, :user_not_authenticated, :unknown_data_source_id,
|
8
|
+
:access_denied, :unsupported_query_operation, :invalid_query,
|
9
|
+
:invalid_request, :internal_error, :not_supported,
|
10
|
+
:illegal_formatting_patterns, :other
|
11
|
+
])
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
def initialize *args
|
15
|
+
@errors = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Checks whether this instance is valid (in terms of configuration parameters)
|
19
|
+
# or not.
|
20
|
+
def valid?
|
21
|
+
validate
|
22
|
+
@errors.size == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Placeholder to be overwritten by subclasses
|
26
|
+
def validate
|
27
|
+
end
|
28
|
+
|
29
|
+
# Manually adds a new validation error. +reason+ should be a symbol detailing
|
30
|
+
# the cause of the errors and should be one of the +ERROR_REASONS+.
|
31
|
+
# +message+ is a short descriptive message, while +detailed_message+, if
|
32
|
+
# provided, can be a longer message that can include minimal html formatting
|
33
|
+
# (anchor tags with a single href attribute).
|
34
|
+
def add_error(reason, message, detailed_message=nil)
|
35
|
+
unless @@error_reasons.include?(reason)
|
36
|
+
raise ArgumentError.new("Invalid error reason: #{reason}")
|
37
|
+
end
|
38
|
+
error = {:reason => reason.to_s, :message => message}
|
39
|
+
error[:detailed_message] = detailed_message if detailed_message
|
40
|
+
@errors << error
|
41
|
+
return self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Hotwire
|
2
|
+
class Request < Hotwire::Base
|
3
|
+
|
4
|
+
# Factory method to create a Request instance from the request parameters.
|
5
|
+
# +tqx:out+ is used to determine the specific class instance returned
|
6
|
+
# by this method, depending on the requested output format.
|
7
|
+
def self.from_params(params)
|
8
|
+
# Exract Wire params from the request.
|
9
|
+
wire_params = {}
|
10
|
+
tqx = params[:tqx]
|
11
|
+
return false if not tqx or tqx.blank?
|
12
|
+
|
13
|
+
wire_params[:tqx] = true
|
14
|
+
tqx.split(';').each do |kv|
|
15
|
+
key, value = kv.split(':')
|
16
|
+
wire_params[key.to_sym] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create the appropriate Wire instance from the gviz-specific parameters
|
20
|
+
wire_params[:out] ||= "json"
|
21
|
+
self.new(wire_params)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a new instance and validates it.
|
25
|
+
def initialize(wire_params)
|
26
|
+
super
|
27
|
+
@params = wire_params
|
28
|
+
end
|
29
|
+
protected :initialize
|
30
|
+
|
31
|
+
# Builds a new response object from the request
|
32
|
+
def build_response
|
33
|
+
Hotwire::Response.from_request(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validates this instance by checking that the configuration parameters
|
37
|
+
# conform to the official specs.
|
38
|
+
def validate
|
39
|
+
@errors.clear
|
40
|
+
if @params[:tqx]
|
41
|
+
add_error(:invalid_request,
|
42
|
+
"Missing required parameter reqId") unless @params[:reqId]
|
43
|
+
|
44
|
+
if @params[:version] && !Hotwire.supported_api_versions.include?(@params[:version])
|
45
|
+
add_error(:invalid_request, "Unsupported version #{@params[:version]}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Access a Wire parameter. +k+ must be symbols, like +:out+, +:reqId+.
|
51
|
+
def [](k)
|
52
|
+
@params[k]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets a Wire parameter. +k+ must be symbols, like +:out+, +:reqId+.
|
56
|
+
def []=(k, v)
|
57
|
+
@params[k] = v
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Hotwire
|
2
|
+
module Response
|
3
|
+
# Factory method to create a Hotwire::Response instance from a Hotwire::Request
|
4
|
+
# object
|
5
|
+
def self.from_request(request)
|
6
|
+
case request[:out]
|
7
|
+
when "json"
|
8
|
+
Hotwire::Response::Json.new(request)
|
9
|
+
when "html"
|
10
|
+
Hotwire::Response::Html.new(request)
|
11
|
+
when "csv"
|
12
|
+
Hotwire::Response::Csv.new(request)
|
13
|
+
else
|
14
|
+
Hotwire::Response::Invalid.new(request)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Hotwire
|
2
|
+
module Response
|
3
|
+
class Base < Hotwire::Base
|
4
|
+
attr_reader :data, :columns
|
5
|
+
# Creates a new instance and validates it.
|
6
|
+
def initialize(request)
|
7
|
+
super
|
8
|
+
@request = request
|
9
|
+
@columns = []
|
10
|
+
@data = []
|
11
|
+
@version = "0.5"
|
12
|
+
@coltypes = [ "boolean", "number", "string", "date", "datetime", "timeofday"]
|
13
|
+
@colkeys = [ :type, :id, :label, :pattern]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adds a new column to the visualization. This method can be invoked as many
|
17
|
+
# times as the number of columns to export in the visualiation. Invocations can
|
18
|
+
# be chained.
|
19
|
+
#
|
20
|
+
# Invocation order is important! The same order will be expected when setting
|
21
|
+
# the actual data and to produce the response.
|
22
|
+
#
|
23
|
+
# +type+ must be one of the supported Wire datatypes, or an +ArgumentError+ will
|
24
|
+
# be raised. +params+ is an optional map to define extra column attributes.
|
25
|
+
# These include: +:id+, +:label+ and +:pattern+.
|
26
|
+
def add_column(type, params=nil)
|
27
|
+
raise ArgumentError.new("Invalid column type: #{type}") if !@coltypes.include?(type)
|
28
|
+
params ||= {}
|
29
|
+
params[:type] = type
|
30
|
+
|
31
|
+
# TODO: passing a wront type in params bypasses the previous type check.
|
32
|
+
@columns << params.delete_if { |k,v| !@colkeys.include?(k) }
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
alias_method :add_col, :add_column
|
36
|
+
|
37
|
+
# Sets the data to be exported. +data+ should be a 2-dimensional array. The
|
38
|
+
# first index should iterate over rows, the second over columns. Column
|
39
|
+
# ordering must be the same used in +add_col+ invokations.
|
40
|
+
#
|
41
|
+
# Anything that behaves like a 2-dimensional array and supports +each+ is
|
42
|
+
# a perfectly fine alternative.
|
43
|
+
def set_data(data)
|
44
|
+
@data = data
|
45
|
+
return self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Placeholder method for the subclasses to overwrite
|
49
|
+
def body
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hotwire
|
2
|
+
module Response
|
3
|
+
# Specialization responsible for producing responses in CSV format.
|
4
|
+
class Csv < Hotwire::Response::Base
|
5
|
+
def body
|
6
|
+
rsp = []
|
7
|
+
CSV::Writer.generate(rsp) do |csv|
|
8
|
+
csv << columns.map { |col| col[:label] || col[:id] || col[:type] }
|
9
|
+
data.each do |datarow|
|
10
|
+
csv << datarow
|
11
|
+
end
|
12
|
+
end
|
13
|
+
return rsp.join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Hotwire
|
2
|
+
module Response
|
3
|
+
# Specialization responsible for producing responses in HTML format.
|
4
|
+
# Invoking +response+ on this class doesn't do anything, as this class
|
5
|
+
# should be passed to a view and generate HTML over there.
|
6
|
+
class Html < Hotwire::Response::Base
|
7
|
+
# no need to answer anything ... response rendering is handled by
|
8
|
+
# an erb template.
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hotwire
|
2
|
+
module Response
|
3
|
+
# Represents an invalid set of parameters. This is used only to guarantee
|
4
|
+
# a return value from the factory methods, even when parameters are wrong.
|
5
|
+
#
|
6
|
+
# Invoking +valid?+ on this class will sistematically return +false+.
|
7
|
+
class Invalid < Hotwire::Response::Base
|
8
|
+
def initialize(request)
|
9
|
+
super(request)
|
10
|
+
add_error(:invalid_request,
|
11
|
+
"Invalid output format: #{request[:out]}. " +
|
12
|
+
"Valid ones are json,csv,html")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|