Skip to content

Commit

Permalink
v0.0.3: def_prepared/statement.as(:csv)
Browse files Browse the repository at this point in the history
  • Loading branch information
dslh committed Oct 13, 2015
1 parent 1a306cb commit 07aad60
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 11 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
0.0.3 (2015-10-13)
==================

* Configurable method framework. Adds `def_prepared/statement.as(:csv)` - [@dslh](https://github.com/dslh)

0.0.2 (2015-10-12)
==================

* Adds `def_statement` to compliment `def_prepared` - [@dslh](https://github.com/dslh).

0.0.1 (2015-10-11)
==================

* Initial release - [@dslh](https://github.com/dslh).
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,23 @@ Db::ComplicatedStuff.prepare_all_statements my_conn
the SQL queries are saved in memory rather than being sent to the database
at connection time. This is useful for queries that will only be run once
or twice during a program's execution.

#### Configuring defined statements

`db_mod` contains a simple framework for extending these statement methods
and prepared methods with additional argument and result processing. For
now only `.as(:csv)` is supported, which will cause the method to format
the SQL result set as a CSV string.

```ruby
module CsvReports
include DbMod

def_prepared(:foo, 'SELECT a, b FROM foo WHERE bar_id = $id').as(:csv)
end

include CsvReports
db_connect db: 'testdb'

foo(id: 1) # => "a,b\n1,2\n3,4\n..."
```
38 changes: 38 additions & 0 deletions lib/db_mod/as.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require_relative 'as/csv'

module DbMod
# Contains coercers and other functions that allow
# module instance methods returning an SQL result set
# to be extended with additional result coercion and
# formatting. The normal way to access this functionality
# is via {DbMod::Statements::ConfigurableMethod#as},
# which is available when defining a statement method
# or prepared method:
#
# def_statement(:a, 'SELECT a, b, c FROM foo').as(:csv)
# def_prepared(:b, 'SELECT d, e, f FROM bar').as(:csv)
module As
# List of available result coercion methods.
# Only keys defined here are allowed as arguments
# to {DbMod::Statements::ConfigurableMethod#as}.
COERCERS = {
csv: DbMod::As::Csv
}

# Extend a method so that the SQL result set it
# returns will be coerced to the given type.
# See {COERCERS} for a list of defined coercion
# methods.
#
# @param mod [Module] module where the method has been defined
# @param name [Symbol] method name
# @param type [Symbol] type to which result set should be coerced
def self.extend_method(mod, name, type)
unless COERCERS.key? type
fail ArgumentError, "#{type} not in #{COERCERS.keys.join ', '}"
end

Statements.extend_method(mod, name, COERCERS[type])
end
end
end
37 changes: 37 additions & 0 deletions lib/db_mod/as/csv.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module DbMod
module As
# Coercer which converts an SQL result set
# into a string formatted as a CSV document.
# May be enabled for a prepared method or
# statement method using +.as(:csv)+:
#
# def_statement(:a, 'SELECT a, b FROM foo').as(:csv)
# def_prepared(:b, 'SELECT b, c FROM bar').as(:csv)
#
# def do_stuff
# a # => "a,b\r\n1,2\r\n3,4\r\n..."
# end
module Csv
# Enables this module to be passed to {DbMod::Statements.extend_method}
# as the +wrapper+ function, in which case it will retrieve the results
# and format them as a CSV document using the column names from the
# result set.
#
# @param wrapped_method [Method] the method that has been wrapped
# @param args [*] arguments expected to be passed to the wrapped method
# @return [String] a CSV formatted document
def self.call(wrapped_method, *args)
results = wrapped_method.call(*args)

headers = nil
CSV.generate do |csv|
results.each do |row|
csv << (headers = row.keys) unless headers

csv << headers.map { |col| row[col] }
end
end
end
end
end
end
38 changes: 38 additions & 0 deletions lib/db_mod/statements.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require_relative 'statements/configurable_method'
require_relative 'statements/statement'
require_relative 'statements/prepared'

Expand All @@ -15,5 +16,42 @@ def self.setup(mod)
DbMod::Statements::Prepared.setup(mod)
DbMod::Statements::Statement.setup(mod)
end

# Used by submodules to when defining a method as declared by
# +def_statement+ or +def_prepared+. Wraps the defined method
# so that it may be extended with additional argument and
# result processing.
#
# @param mod [Module] the module where the method has been declared
# @param name [Symbol] the name of the module that has been defined
# @param definition [Proc] method definition
# @return [DbMod::Statements::ConfigurableMethod] dsl object for
# further extending the method
def self.configurable_method(mod, name, definition)
mod.instance_eval { define_method(name, definition) }

ConfigurableMethod.new(mod, name)
end

# Used by {ConfigurableMethod} (and associated code) to wrap a defined
# statement method or prepared method with additional parameter or result
# processing. A wrapper method definition should be provided, which will
# be called in place of the original method. It will be called with the
# original method proc as a first argument followed by the original
# method arguments (before +DbMod+ has made any attempt to validate them!).
# It is expected to yield to the original proc at some point, although it
# is allowed to do whatever it wants with the results before returning them.
#
# @param mod [Module] the module where the method has been defined
# @param name [Symbol] the method name
# @param wrapper [Proc] a function which will be used to wrap the
# original method definition
def self.extend_method(mod, name, wrapper)
mod.instance_eval do
wrapped = instance_method(name)

define_method name, ->(*args) { wrapper.call wrapped.bind(self), *args }
end
end
end
end
41 changes: 41 additions & 0 deletions lib/db_mod/statements/configurable_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'db_mod/as'

module DbMod
module Statements
# Encapsulates a method that has just been defined
# via the dsl exposed in {DbMod::Statements} so that
# it can be extended with additional processing such
# as result coercion.
#
# The pattern here is something similar to rack's middleware.
# Calling any of the extension methods below will replace
# the original method defined by +def_prepared+ or +def_statement+
# with a wrapper function that may perform processing on given
# arguments, pass them to the original function, then perform
# additional processing on the result.
class ConfigurableMethod
# Encapsulate a method that has been newly defined
# by a {DbMod} dsl function, for additional configuration.
#
# @param mod [Module] the {DbMod} enabled module
# where the method was defined
# @param name [Symbol] the method name
def initialize(mod, name)
@mod = mod
@name = name
end

# Extend the method by converting results into a given
# format, using one of the coercion methods defined
# under {DbMod::As}.
#
# @param type [Symbol] for now, only :csv is accepted
# @return [self]
def as(type)
DbMod::As.extend_method(@mod, @name, type)

self
end
end
end
end
9 changes: 4 additions & 5 deletions lib/db_mod/statements/prepared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,8 @@ def self.define_prepared_method(mod, name, params)
# @param name [Symbol] name of the method to be defined
# and the prepared query to be called.
def self.define_no_args_prepared_method(mod, name)
mod.instance_eval do
define_method name, ->() { conn.exec_prepared(name.to_s) }
end
method = ->() { conn.exec_prepared(name.to_s) }
Statements.configurable_method mod, name, method
end

# Define a method with the given name that accepts the
Expand All @@ -165,7 +164,7 @@ def self.define_named_args_prepared_method(mod, name, params)
conn.exec_prepared(name.to_s, args)
end

mod.instance_eval { define_method(name, method) }
Statements.configurable_method mod, name, method
end

# Define a method with the given name that accepts a fixed
Expand All @@ -186,7 +185,7 @@ def self.define_fixed_args_prepared_method(mod, name, count)
conn.exec_prepared(name.to_s, args)
end

mod.instance_eval { define_method(name, method) }
Statements.configurable_method(mod, name, method)
end

# Adds +prepared_statements+ to a module. This list of named
Expand Down
8 changes: 3 additions & 5 deletions lib/db_mod/statements/statement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def self.define_statement_method(mod, name, params, sql)
# @param name [Symbol] name of the method to be defined
# @param sql [String] parameterless SQL statement to execute
def self.define_no_args_statement_method(mod, name, sql)
mod.instance_eval do
define_method name, ->() { query(sql) }
end
Statements.configurable_method mod, name, ->() { query(sql) }
end

# Define a method with the given name, that accepts the
Expand All @@ -101,7 +99,7 @@ def self.define_named_args_statement_method(mod, name, params, sql)
conn.exec_params(sql, args)
end

mod.instance_eval { define_method(name, method) }
Statements.configurable_method mod, name, method
end

# Define a method with the given name that accepts a fixed number
Expand All @@ -119,7 +117,7 @@ def self.define_fixed_args_statement_method(mod, name, count, sql)
conn.exec_params(sql, args)
end

mod.instance_eval { define_method(name, method) }
Statements.configurable_method mod, name, method
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/db_mod/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Version information
module DbMod
# The current version of db_mod.
VERSION = '0.0.2'
VERSION = '0.0.3'
end
36 changes: 36 additions & 0 deletions spec/db_mod/as/csv_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'spec_helper'
require 'csv'

describe DbMod::As::Csv do
subject do
Module.new do
include DbMod

def_statement(:statement, 'SELECT a, b FROM foo').as(:csv)
def_prepared(:prepared, 'SELECT c, d FROM bar').as(:csv)
end
end

before do
@conn = instance_double 'PGconn'
allow(@conn).to receive(:prepare)
allow(PGconn).to receive(:connect).and_return @conn
end

{
statement: :query,
prepared: :exec_prepared
}.each do |method_type, exec_type|
context "#{method_type} methods" do
it 'coerces results to csv' do
expect(@conn).to receive(exec_type).and_return([
{ 'a' => '1', 'b' => '2' },
{ 'a' => '3', 'b' => '4' }
])

csv = subject.create(db: 'testdb').send(method_type)
expect(csv).to eq("a,b\n1,2\n3,4\n")
end
end
end
end
14 changes: 14 additions & 0 deletions spec/db_mod/as_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'spec_helper'

# See submodules for more
describe DbMod::As do
it 'disallows unknown coercions' do
expect do
Module.new do
include DbMod

def_statement(:foo, 'SELECT 1').as(:lolwut)
end
end.to raise_exception ArgumentError
end
end

0 comments on commit 07aad60

Please sign in to comment.