hotwire 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0.0
1
+ 0.1.0
@@ -5,39 +5,44 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{hotwire}
8
- s.version = "0.0.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-20}
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.makrdown"
17
+ "README.markdown"
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
21
21
  ".gitignore",
22
22
  "LICENSE",
23
- "README.makrdown",
23
+ "README.markdown",
24
24
  "Rakefile",
25
25
  "VERSION",
26
26
  "hotwire.gemspec",
27
27
  "lib/hotwire.rb",
28
- "lib/hotwire/active_record_mixins.rb",
29
- "lib/hotwire/column_headers.rb",
30
- "lib/hotwire/core_extensions.rb",
31
- "lib/hotwire/row.rb",
32
- "lib/hotwire/table.rb",
33
- "test/active_record_test_helper.rb",
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/test_row.rb",
40
- "test/test_table.rb"
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/active_record_test_helper.rb",
49
- "test/hotwire_test.rb",
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/test_row.rb",
55
- "test/test_table.rb"
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
 
@@ -1,8 +1,14 @@
1
- require 'hotwire/core_extensions'
2
- require 'hotwire/column_headers'
3
- require 'hotwire/row'
4
- require 'hotwire/table'
1
+ module Hotwire
2
+ def self.supported_api_versions
3
+ ["0.5"]
4
+ end
5
+ end
5
6
 
6
- if defined? ActiveRecord::Base
7
- require 'hotwire/active_record_mixins.rb'
8
- end
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'
@@ -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