omnis 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +14 -0
- data/README.md +130 -1
- data/lib/omnis.rb +7 -3
- data/lib/omnis/mongo_query.rb +79 -0
- data/lib/omnis/mongo_report.rb +13 -0
- data/lib/omnis/nested_hash_extractor.rb +29 -0
- data/lib/omnis/operators.rb +36 -0
- data/lib/omnis/query.rb +67 -0
- data/lib/omnis/transformer.rb +86 -0
- data/lib/omnis/version.rb +1 -1
- data/omnis.gemspec +6 -2
- data/spec/lib/omnis/mongo_query_spec.rb +109 -0
- data/spec/lib/omnis/nested_hash_extractor_spec.rb +32 -0
- data/spec/lib/omnis/operators_spec.rb +23 -0
- data/spec/lib/omnis/query_spec.rb +65 -0
- data/spec/lib/omnis/transformer_spec.rb +71 -0
- data/spec/spec.rake +2 -2
- data/spec/spec_helper.rb +2 -0
- metadata +96 -10
- data/readme.rb +0 -49
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :cli => "--color --format nested --fail-fast", :notification => true,
|
5
|
+
:run_all => { :cli => "--color --fail-fast" } do
|
6
|
+
watch(%r{^spec/.+_spec\.rb$})
|
7
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
|
10
|
+
# Turnip features and steps
|
11
|
+
# watch(%r{^spec/acceptance/(.+)\.feature$})
|
12
|
+
# watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
13
|
+
end
|
14
|
+
|
data/README.md
CHANGED
@@ -1,6 +1,135 @@
|
|
1
1
|
# Omnis
|
2
|
+
The goal is to simplify standard and repetetive queries to Mongo and presenting their results.
|
3
|
+
To do this Omnis provides a Query and a Transformer, both can be configured using a DSL.
|
2
4
|
|
3
|
-
|
5
|
+
## Query
|
6
|
+
Converts a params Hash into Operators to be able to easily build queries against databases et al. This is a generic way to process incoming parameters.
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
{ "ref_anixe" => "1abc"}
|
10
|
+
```
|
11
|
+
becomes
|
12
|
+
```ruby
|
13
|
+
Matches.new(:ref_anixe, "1abc")
|
14
|
+
```
|
15
|
+
|
16
|
+
Example:
|
17
|
+
```ruby
|
18
|
+
class SomeQuery
|
19
|
+
include Omnis::Query
|
20
|
+
|
21
|
+
def self.parse_date(params, name)
|
22
|
+
param = params[name]
|
23
|
+
return nil if param.nil?
|
24
|
+
time = Time.parse(param)
|
25
|
+
Between.new(name, time.getlocal.beginning_of_day..time.getlocal.end_of_day)
|
26
|
+
end
|
27
|
+
|
28
|
+
param :ref_anixe, Matches
|
29
|
+
param :passenger, Equals
|
30
|
+
param(:date, Between) {|params| self.parse_date(params, :date) }
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
If a lambda used for extraction returns `nil`, the parameter will be removed.
|
35
|
+
|
36
|
+
Params also support defaults as values or as lambdas which will be executed at the time the extraction of the values happens. This way you can build pre-defined queries and if required only override some values. The difference to normal blocks for extraction is that, the latter is not called if the param is not in the inputs - in this case the default will be used.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
param :date_from, Between, :default => Between.new("services.date_from", tomorrow.beginning_of_day..tomorrow.end_of_day)
|
40
|
+
param :contract, Matches, :default => "^wotra."
|
41
|
+
```
|
42
|
+
|
43
|
+
## MongoQuery
|
44
|
+
This covers a standard use case where you have a bunch of params in a Hash, for instance from a web request and you need validation, and transformation of the incoming values.
|
45
|
+
No actual calls to mongo are done.
|
46
|
+
|
47
|
+
Example:
|
48
|
+
```ruby
|
49
|
+
class BookingQuery
|
50
|
+
include Omnis::MongoQuery
|
51
|
+
|
52
|
+
# collection Mongo::Connection.new['bms']['bookings'] # planned!?
|
53
|
+
|
54
|
+
param :ref_anixe, Equals
|
55
|
+
param :contract, Matches
|
56
|
+
param :description, Matches
|
57
|
+
param :status, Matches
|
58
|
+
param :product, BeginsWith
|
59
|
+
param :agency, Equals
|
60
|
+
|
61
|
+
# if this param is in the query, fetch the field "ref_customer"
|
62
|
+
param :ref_customer, Matches, :field => "ref_customer"
|
63
|
+
|
64
|
+
# those fields are always fetched
|
65
|
+
fields %w[ref_anixe contract description status product agency passengers date_status_modified services]
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Usage:
|
70
|
+
```ruby
|
71
|
+
query = BookingQuery.new("ref_anixe" => "1abc", "product" => "HOT")
|
72
|
+
mongo = query.to_mongo
|
73
|
+
Mongo::Connection.new['bms']['bookings'].find(mongo.selector, mongo.opts)
|
74
|
+
```
|
75
|
+
|
76
|
+
## Transformer
|
77
|
+
Transforms some data into another form of (flattened) data. Extractors can be used to get values from the data source.
|
78
|
+
If the first parameter of a property denotes the output field, the second is a string which is passed as argument to the extractor.
|
79
|
+
|
80
|
+
Example:
|
81
|
+
```ruby
|
82
|
+
class BookingTransformer
|
83
|
+
include Omnis::DataTransformer
|
84
|
+
extractor Omnis::NestedHashExtractor.new
|
85
|
+
|
86
|
+
property :ref_anixe, "ref_anixe"
|
87
|
+
property :ref_customer, "ref_customer"
|
88
|
+
property :status, "status"
|
89
|
+
property(:passenger) {|doc| Maybe(doc)['passengers'].map {|v| v.first.values.slice(1..2).join(' ') }.or('Unknown').fetch.to_s }
|
90
|
+
property :date "date_status_modified", :default => Time.at(0), :format => ->v { v.to_s(:date) }
|
91
|
+
property :description, "description"
|
92
|
+
property :product, "product"
|
93
|
+
property :contract, "contract"
|
94
|
+
property :agency, "agency"
|
95
|
+
property :date_from, "services.0.date_from", :default => "n/a", :format => ->v { v.to_s(:date) }
|
96
|
+
property :date_to, "services.0.date_to", :default => "n/a", :format => ->v { v.to_s(:date) }
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
Usage:
|
101
|
+
```ruby
|
102
|
+
transformer = BookingTransformer.new
|
103
|
+
transformer.transform(doc)
|
104
|
+
```
|
105
|
+
This will produce a Hash like `{:ref_anixe => "1abc", :status => "book_confirmed" ... }`
|
106
|
+
|
107
|
+
If you provide blocks for all properties, an Extractor is not required
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class ExtractorlessTransformer
|
111
|
+
include Omnis::DataTransformer
|
112
|
+
property(:ref) {|src| src["ref_anixe"] }
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
If you provide a `#to_object(hash)` method in the Transformer definition, it will be used to convert the output Hash into the object of you desire.
|
117
|
+
|
118
|
+
## Putting it all together
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
query = BookingQuery.new("ref_anixe" => "1abc", "product" => "HOT").to_mongo
|
122
|
+
transformer = BookingTransformer.new.to_proc
|
123
|
+
collection = Mongo::Connection.new['bms']['bookings']
|
124
|
+
|
125
|
+
table = collection.find(query.selector, query.opts.merge(:transformer => transformer))
|
126
|
+
|
127
|
+
table = Omnis::MongoTable.new(connection, params, BookingQuery, BookingTransformer)
|
128
|
+
|
129
|
+
table.call.each do |row|
|
130
|
+
row.
|
131
|
+
end
|
132
|
+
```
|
4
133
|
|
5
134
|
## Installation
|
6
135
|
|
data/lib/omnis.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require "omnis/version"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'omnis/nested_hash_extractor'
|
5
|
+
require 'omnis/transformer'
|
6
|
+
require 'omnis/operators'
|
7
|
+
require 'omnis/query'
|
8
|
+
require 'omnis/mongo_query'
|
9
|
+
require 'omnis/mongo_report'
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Omnis
|
4
|
+
module MongoQuery
|
5
|
+
include Omnis::Query
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
include InstanceMethods
|
9
|
+
extend ClassMethods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
include Omnis::Query::ClassMethods
|
15
|
+
|
16
|
+
attr_reader :page_param_name, :items_per_page
|
17
|
+
def field_list
|
18
|
+
@fields ||= []
|
19
|
+
end
|
20
|
+
def fields(list)
|
21
|
+
field_list.concat(list)
|
22
|
+
end
|
23
|
+
|
24
|
+
def page(page_param_name, opts={})
|
25
|
+
@page_param_name = page_param_name
|
26
|
+
@items_per_page = opts[:items_per_page] || 10
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
include Omnis::Operators
|
32
|
+
|
33
|
+
def to_mongo
|
34
|
+
extracted_params = extract
|
35
|
+
OpenStruct.new({ :selector => mongo_selector(extracted_params),
|
36
|
+
:opts => mongo_opts(extracted_params)})
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def page
|
41
|
+
return 0 unless @input_params.has_key? page_param_name
|
42
|
+
return @input_params[page_param_name].to_i - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
def skip
|
46
|
+
page * items_per_page
|
47
|
+
end
|
48
|
+
|
49
|
+
def page_param_name
|
50
|
+
self.class.page_param_name || :page
|
51
|
+
end
|
52
|
+
|
53
|
+
def items_per_page
|
54
|
+
self.class.items_per_page || 20
|
55
|
+
end
|
56
|
+
|
57
|
+
def mongo_operator(operator)
|
58
|
+
case operator
|
59
|
+
when Equals; operator.value
|
60
|
+
when Matches; /#{operator.value}/i
|
61
|
+
when BeginsWith; /^#{operator.value}/i
|
62
|
+
when Between; { :'$gte' => operator.value.begin, :'$lt' => operator.value.end}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def mongo_selector(extracted_params)
|
67
|
+
Hash[extracted_params.map { |operator| [operator.key, mongo_operator(operator)] }]
|
68
|
+
end
|
69
|
+
|
70
|
+
def mongo_opts(extracted_params)
|
71
|
+
params_with_extra_fields = extracted_params.collect(&:opts).select {|e| e.has_key? :field}
|
72
|
+
extra_fields = params_with_extra_fields.map {|e| e[:field]}
|
73
|
+
fields = self.class.field_list.concat(extra_fields)
|
74
|
+
{ :limit => items_per_page, :skip => skip, :fields => fields }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'monadic'
|
2
|
+
|
3
|
+
module Omnis
|
4
|
+
class NestedHashExtractor
|
5
|
+
# returns a lambda which extracts a value from a nested hash
|
6
|
+
def extractor(path)
|
7
|
+
raise ArgumentError("path to extract must be a string") unless String === path
|
8
|
+
expr = "source#{from_dot_path(path)} rescue Nothing"
|
9
|
+
->source { eval(expr) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
# convert from a path to a ruby expression (as string)
|
14
|
+
def from_dot_path(path)
|
15
|
+
return nil if path.nil?
|
16
|
+
path.split('.').map {|i| field(i) }.join
|
17
|
+
end
|
18
|
+
|
19
|
+
def field(f)
|
20
|
+
return '[' << f << ']' if is_i?(f)
|
21
|
+
return '.fetch("' << f << '", Nothing)'
|
22
|
+
end
|
23
|
+
|
24
|
+
# checks if the string is a number
|
25
|
+
def is_i?(s)
|
26
|
+
!!(s =~ /^[-+]?[0-9]+$/)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Omnis
|
2
|
+
module Operators
|
3
|
+
class NullOperator
|
4
|
+
attr_reader :key, :value, :opts
|
5
|
+
def initialize(key, value, opts={})
|
6
|
+
@key, @value, @opts = key, value, opts
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
return false unless other.is_a? self.class
|
11
|
+
return false unless @key == other.key
|
12
|
+
@value == other.value
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
klas = self.class.to_s.downcase.split('::')[-1]
|
17
|
+
"#{@key.to_s} #{klas} #{@value}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Matches < NullOperator
|
22
|
+
end
|
23
|
+
|
24
|
+
class Equals < NullOperator
|
25
|
+
end
|
26
|
+
|
27
|
+
class Gte < NullOperator
|
28
|
+
end
|
29
|
+
|
30
|
+
class Between < NullOperator
|
31
|
+
end
|
32
|
+
|
33
|
+
class BeginsWith < NullOperator
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/omnis/query.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_support/core_ext/hash'
|
2
|
+
|
3
|
+
module Omnis
|
4
|
+
module Query
|
5
|
+
|
6
|
+
class Param
|
7
|
+
attr_reader :name, :operator
|
8
|
+
def initialize(name, operator, opts={}, &extractor)
|
9
|
+
# raise ArgumentError("operator must be a descendant of Omnis::Operators::NullOperator") unless operator.is_a? Omnis::Operators::NullOperator
|
10
|
+
extractor ||= ->params { params[name] }
|
11
|
+
@name, @operator, @opts, @extractor = name, operator, opts, extractor
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
return false unless other.is_a? self.class
|
16
|
+
return false unless @name == other.name
|
17
|
+
@operator == other.operator
|
18
|
+
end
|
19
|
+
|
20
|
+
# extracts the value for a param, using the extractor lamba or the default value
|
21
|
+
def extract(params)
|
22
|
+
value = @extractor.(params) || default
|
23
|
+
return value if value.is_a? Omnis::Operators::NullOperator
|
24
|
+
return @operator.new(@name, value, @opts) unless value.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def default
|
28
|
+
expr = @opts[:default]
|
29
|
+
return expr.call if expr.is_a? Proc
|
30
|
+
return expr
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.included(base)
|
35
|
+
base.class_eval do
|
36
|
+
extend ClassMethods
|
37
|
+
include InstanceMethods
|
38
|
+
include Omnis::Operators
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def params
|
44
|
+
@params ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def param(name, operator, opts={}, &block)
|
48
|
+
Omnis::Query::Param.new(name, operator, opts, &block).tap do |param|
|
49
|
+
params[param.name] = param
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
module InstanceMethods
|
54
|
+
def initialize(input_params)
|
55
|
+
@input_params = input_params.symbolize_keys
|
56
|
+
end
|
57
|
+
|
58
|
+
def fetch(name)
|
59
|
+
self.class.params.fetch(name).extract(@input_params)
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract
|
63
|
+
self.class.params.map { |k,v| v.extract(@input_params) }.compact
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Omnis
|
2
|
+
module Transformer
|
3
|
+
class Property
|
4
|
+
attr_reader :name, :expr, :opts
|
5
|
+
def initialize(name, expr, opts, extractor)
|
6
|
+
@name, @expr, @opts, @extractor = name, expr, opts, extractor
|
7
|
+
end
|
8
|
+
|
9
|
+
def default
|
10
|
+
opts[:default]
|
11
|
+
end
|
12
|
+
|
13
|
+
def format
|
14
|
+
opts[:format]
|
15
|
+
end
|
16
|
+
|
17
|
+
def extract(source)
|
18
|
+
@extractor.call(source)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.class_eval do
|
24
|
+
extend ClassMethods
|
25
|
+
include InstanceMethods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def properties
|
31
|
+
@properties ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
# if an expr is provided it will be passed to the configured extractor,
|
35
|
+
# otherwise a block is required
|
36
|
+
def property(name, expr=nil, opts={}, &block)
|
37
|
+
raise ArgumentError if (expr.nil? && block.nil?)
|
38
|
+
|
39
|
+
xtr = case expr
|
40
|
+
when String; @extractor.extractor(expr)
|
41
|
+
when nil ; block
|
42
|
+
end
|
43
|
+
|
44
|
+
Omnis::Transformer::Property.new(name, expr, opts, xtr).tap do |prop|
|
45
|
+
properties[prop.name] = prop
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def extractor(obj)
|
50
|
+
@extractor = obj
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract(source, expr)
|
54
|
+
@extractor.extractor(expr).call(source)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
def __extract(property, source)
|
60
|
+
value = property_value(property, source)
|
61
|
+
if property.format
|
62
|
+
property.format.call(value)
|
63
|
+
else
|
64
|
+
value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def property_value(property, source)
|
69
|
+
value = property.extract(source)
|
70
|
+
return property.default if property.default && (value == Nothing || value.nil?)
|
71
|
+
return value
|
72
|
+
end
|
73
|
+
|
74
|
+
def transform(source)
|
75
|
+
result = Hash[self.class.properties.map do |k, v| [k, __extract(v, source)] end]
|
76
|
+
respond_to?(:to_object) ? to_object(result) : result
|
77
|
+
end
|
78
|
+
|
79
|
+
# provides a Proc to the transform method, for use e.g. with Mongo documents
|
80
|
+
# If you want to cache a transformer for reuse, you can cache just this Proc
|
81
|
+
def to_proc
|
82
|
+
method(:transform).to_proc
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/omnis/version.rb
CHANGED
data/omnis.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Omnis::VERSION
|
9
9
|
gem.authors = ["Piotr Zolnierek"]
|
10
10
|
gem.email = ["pz@anixe.pl"]
|
11
|
-
gem.description = %q{Helps a read-only ORM kind-of}
|
11
|
+
gem.description = %q{Helps with a read-only ORM kind-of, more useful than the description}
|
12
12
|
gem.summary = %q{see above}
|
13
13
|
gem.homepage = "http://github.com/pzol/omnis"
|
14
14
|
|
@@ -16,11 +16,15 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
|
+
gem.add_dependency 'activesupport'
|
20
|
+
gem.add_dependency 'bson_ext', '>=1.7.0'
|
21
|
+
gem.add_dependency 'monadic'
|
22
|
+
gem.add_dependency 'mongo', '>=1.7.0'
|
19
23
|
gem.add_development_dependency 'rspec', '>=2.9.0'
|
20
24
|
gem.add_development_dependency 'guard'
|
21
25
|
gem.add_development_dependency 'guard-rspec'
|
22
26
|
gem.add_development_dependency 'guard-bundler'
|
23
27
|
gem.add_development_dependency 'growl'
|
24
|
-
gem.add_development_dependency 'activesupport'
|
25
28
|
gem.add_development_dependency 'rake'
|
29
|
+
gem.add_development_dependency 'rb-fsevent', '~> 0.9.1'
|
26
30
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'omnis/operators'
|
3
|
+
require 'omnis/query'
|
4
|
+
require 'omnis/mongo_query'
|
5
|
+
require 'active_support/core_ext/date/calculations'
|
6
|
+
require 'active_support/core_ext/time/calculations'
|
7
|
+
|
8
|
+
describe Omnis::MongoQuery do
|
9
|
+
class TestIntegrationQuery
|
10
|
+
include Omnis::MongoQuery
|
11
|
+
|
12
|
+
# collection Mongo::Connection.new['bms']['bookings']
|
13
|
+
def self.parse_date(params, name)
|
14
|
+
param = params[name]
|
15
|
+
return nil if param.nil?
|
16
|
+
time = Time.parse(param)
|
17
|
+
Between.new(name, time.beginning_of_day..time.end_of_day)
|
18
|
+
end
|
19
|
+
|
20
|
+
param :ref_anixe, Equals
|
21
|
+
param(:date, Between) {|source| parse_date(source, :date)}
|
22
|
+
param :contract, Matches
|
23
|
+
param :description, Matches
|
24
|
+
param :status, Matches
|
25
|
+
param :product, BeginsWith
|
26
|
+
param :agency, Equals
|
27
|
+
|
28
|
+
page :page, :items_per_page => 20
|
29
|
+
|
30
|
+
# those fields are always fetched
|
31
|
+
fields %w[ref_anixe contract description status product agency passengers date_status_modified services]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "works altogether" do
|
35
|
+
t = TestIntegrationQuery.new("ref_anixe" => "1abc", "contract" => "test", "product" => "HOT", "page" => "2", "date" => "2012-10-12")
|
36
|
+
t.to_mongo.selector.should == { :ref_anixe => "1abc",
|
37
|
+
:contract => /test/i,
|
38
|
+
:product => /^HOT/i,
|
39
|
+
:date => { :'$gte' => Time.local(2012, 10, 12, 0, 0, 0), :'$lt' => Time.local(2012, 10, 12, 23, 59, 59, 999999.999)}
|
40
|
+
}
|
41
|
+
|
42
|
+
fields = %w[ref_anixe contract description status product agency passengers date_status_modified services]
|
43
|
+
t.to_mongo.opts.should == { :limit => 20, :skip => 20, :fields => fields}
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'fields' do
|
47
|
+
class TestFieldsQuery
|
48
|
+
include Omnis::MongoQuery
|
49
|
+
|
50
|
+
fields %w[ref_anixe status]
|
51
|
+
# if this param is in the query, fetch the field "ref_customer"
|
52
|
+
param :ref_customer, Matches, :field => "ref_customer"
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'extra field not requested when param not present' do
|
56
|
+
t = TestFieldsQuery.new({})
|
57
|
+
t.to_mongo.opts.should == { :limit => 20, :skip => 0, :fields => ['ref_anixe', 'status']}
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'extra field is requested when param is in request' do
|
61
|
+
t = TestFieldsQuery.new({"ref_customer" => "123"})
|
62
|
+
t.to_mongo.opts.should == { :limit => 20, :skip => 0, :fields => ['ref_anixe', 'status', 'ref_customer']}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'paging' do
|
67
|
+
class TestPageDefaultQuery
|
68
|
+
include Omnis::MongoQuery
|
69
|
+
end
|
70
|
+
|
71
|
+
it "page default no page number given" do
|
72
|
+
t = TestPageDefaultQuery.new({})
|
73
|
+
t.to_mongo.opts.should == { :limit => 20, :skip => 0, :fields => []}
|
74
|
+
end
|
75
|
+
it "page defaults and page given" do
|
76
|
+
t = TestPageDefaultQuery.new({"page" => 2})
|
77
|
+
t.to_mongo.opts.should == { :limit => 20, :skip => 20, :fields => []}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'practical use case with predefined params' do
|
82
|
+
class TestHotelsWithDepartureTomorrowQuery
|
83
|
+
include Omnis::MongoQuery
|
84
|
+
def self.tomorrow
|
85
|
+
Time.new(2012, 10, 12, 22, 54, 38)
|
86
|
+
end
|
87
|
+
|
88
|
+
param :date_from, Between, :default => Between.new("services.date_from", tomorrow.beginning_of_day..tomorrow.end_of_day)
|
89
|
+
param :contract, Matches, :default => "^wotra."
|
90
|
+
param :product, Equals, :default => "PACKAGE"
|
91
|
+
param :status, Equals, :default => "book_confirmed"
|
92
|
+
|
93
|
+
page :page, :items_per_page => 9999
|
94
|
+
fields %w[ref_anixe ref_customer status passengers date_status_modified description contract agency services]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should fill params with default" do
|
98
|
+
t = TestHotelsWithDepartureTomorrowQuery.new({})
|
99
|
+
m = p t.to_mongo
|
100
|
+
m.selector[:contract].should == /^wotra./i
|
101
|
+
m.selector[:product].should == "PACKAGE"
|
102
|
+
m.selector[:status].should == "book_confirmed"
|
103
|
+
m.selector['services.date_from'].should == {:'$gte' => Time.new(2012, 10, 12), :'$lt' => Time.local(2012, 10, 12, 23, 59, 59, 999999.999)}
|
104
|
+
m.opts[:limit].should == 9999
|
105
|
+
m.opts[:skip].should == 0
|
106
|
+
m.opts[:fields].should == ["ref_anixe", "ref_customer", "status", "passengers", "date_status_modified", "description", "contract", "agency", "services"]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'omnis/nested_hash_extractor'
|
3
|
+
|
4
|
+
describe Omnis::NestedHashExtractor do
|
5
|
+
let(:xtr) { Omnis::NestedHashExtractor.new }
|
6
|
+
let(:doc) {
|
7
|
+
{ "ref_anixe" => "1abc",
|
8
|
+
"contract" => "test",
|
9
|
+
"agency" => nil,
|
10
|
+
"services" => [ { "date_from" => "2012-10-10"}]}
|
11
|
+
}
|
12
|
+
|
13
|
+
it "extracts values using a path" do
|
14
|
+
xtr.extractor("ref_anixe").call(doc).should == "1abc"
|
15
|
+
xtr.extractor("services.0.date_from").call(doc).should == "2012-10-10"
|
16
|
+
xtr.extractor("agency").call(doc).should be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns Nothing if an expression points to a non-existant key" do
|
20
|
+
xtr.extractor("ref_anixe").call({}).should == Nothing
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns Nothing for a nested path, if an exception would be raised" do
|
24
|
+
xtr.extractor("a.b.c").call({}).should == Nothing
|
25
|
+
xtr.extractor("a.(1]").call({}).should == Nothing
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns nil if the underlying value of a key is nil" do
|
29
|
+
xtr.extractor("agency").call(doc).should be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'omnis/operators'
|
3
|
+
|
4
|
+
describe Omnis::Operators do
|
5
|
+
it 'supports equality' do
|
6
|
+
this = Omnis::Operators::NullOperator.new(:key, :value)
|
7
|
+
that = Omnis::Operators::NullOperator.new(:key, :value)
|
8
|
+
(this == that).should be_true
|
9
|
+
|
10
|
+
other = Omnis::Operators::NullOperator.new(:other, :value)
|
11
|
+
(other == this).should be_false
|
12
|
+
|
13
|
+
another = Omnis::Operators::NullOperator.new(:key, :another)
|
14
|
+
(another == this).should be_false
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should carry additional options' do
|
18
|
+
o = Omnis::Operators::NullOperator.new(:key, :value, {:k => 'v'})
|
19
|
+
expect(o.key).to eq(:key)
|
20
|
+
expect(o.value).to eq(:value)
|
21
|
+
expect(o.opts).to eq({:k => 'v'})
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'omnis/operators'
|
3
|
+
require 'omnis/query'
|
4
|
+
require 'active_support/core_ext/hash'
|
5
|
+
|
6
|
+
describe Omnis::Query::Param do
|
7
|
+
it "supports equality" do
|
8
|
+
Omnis::Query::Param.new(:param, Omnis::Operators::Matches).should == Omnis::Query::Param.new(:param, Omnis::Operators::Matches)
|
9
|
+
Omnis::Query::Param.new(:param, Omnis::Operators::Between).should_not == Omnis::Query::Param.new(:param, Omnis::Operators::Matches)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Omnis::Query do
|
14
|
+
class TestBookingParams
|
15
|
+
include Omnis::Query
|
16
|
+
|
17
|
+
def self.parse_date(params, name)
|
18
|
+
param = params[name]
|
19
|
+
return nil if param.nil?
|
20
|
+
time = Time.parse(param)
|
21
|
+
Between.new(name, time.getlocal.beginning_of_day..time.getlocal.end_of_day)
|
22
|
+
end
|
23
|
+
|
24
|
+
param :ref_anixe, Matches
|
25
|
+
param :passenger, Equals
|
26
|
+
param(:date, Between) {|params| parse_date(params, :date) }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "allows to fetch a single param" do
|
30
|
+
t = TestBookingParams.new({"ref_anixe" => "1abc"})
|
31
|
+
t.fetch(:ref_anixe).should == Omnis::Operators::Matches.new(:ref_anixe, "1abc")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "allows to extract all at once" do
|
35
|
+
t = TestBookingParams.new({"ref_anixe" => "1abc"})
|
36
|
+
t.extract.should == [Omnis::Operators::Matches.new(:ref_anixe, "1abc")]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "allows using blocks for extracing params" do
|
40
|
+
t = TestBookingParams.new({"date" => "2012-10-02"})
|
41
|
+
value = t.fetch(:date).value
|
42
|
+
value.begin.should be_eql Time.local(2012, 10, 02, 0, 0, 0)
|
43
|
+
value.end.should == Time.local(2012, 10, 02, 23, 59, 59, 999999.999)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns default values even if not in the params" do
|
47
|
+
class TestDefaultParams
|
48
|
+
include Omnis::Query
|
49
|
+
param :contract, Matches, :default => "test"
|
50
|
+
end
|
51
|
+
|
52
|
+
t = TestDefaultParams.new({})
|
53
|
+
t.fetch(:contract).value.should == "test"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should accept a lambda as default" do
|
57
|
+
class TestDefaultsWithLambdaParams
|
58
|
+
include Omnis::Query
|
59
|
+
param :contract, Equals, :default => -> { :angry_nerds }
|
60
|
+
end
|
61
|
+
|
62
|
+
t = TestDefaultsWithLambdaParams.new({})
|
63
|
+
t.fetch(:contract).value.should == :angry_nerds
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'monadic'
|
4
|
+
require 'omnis/nested_hash_extractor'
|
5
|
+
require 'omnis/transformer'
|
6
|
+
|
7
|
+
describe Omnis::Transformer do
|
8
|
+
class TestTransformer
|
9
|
+
include Omnis::Transformer
|
10
|
+
extractor Omnis::NestedHashExtractor.new
|
11
|
+
|
12
|
+
property :ref, "ref_anixe"
|
13
|
+
property(:date_from) {|source| Maybe(extract(source, "services.0.date_from")).or(Time.at(0)).fetch }
|
14
|
+
property :date_to, "services.0.date_to", :default => Time.at(0), :format => ->v { v.strftime("%Y-%m-%d") }
|
15
|
+
property :agency, "agency", :default => "000000"
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:doc) {
|
19
|
+
{ "ref_anixe" => "1abc",
|
20
|
+
"contract" => "test",
|
21
|
+
"agency" => nil,
|
22
|
+
"services" => [ { "date_from" => "2012-10-10"}]}
|
23
|
+
}
|
24
|
+
|
25
|
+
it "should read values from the doc" do
|
26
|
+
t = TestTransformer.new
|
27
|
+
t.transform(doc).should == { :ref => "1abc", :date_from => "2012-10-10", :date_to => "1970-01-01", :agency => "000000" }
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should support blocks in properties, without a defined extractor" do
|
31
|
+
class TestTransformerWithBlock
|
32
|
+
include Omnis::Transformer
|
33
|
+
property(:ref) {|src| Maybe(src)["ref_anixe"].fetch }
|
34
|
+
end
|
35
|
+
|
36
|
+
t = TestTransformerWithBlock.new
|
37
|
+
t.transform(doc).should == { :ref => "1abc" }
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should raise an ArgumentError if no block and no expression provided" do
|
41
|
+
expect {
|
42
|
+
class TestTransformerInvalid
|
43
|
+
include Omnis::Transformer
|
44
|
+
property :ref
|
45
|
+
end
|
46
|
+
}.to raise_error ArgumentError
|
47
|
+
end
|
48
|
+
|
49
|
+
it "uses a #to_object method if provided to convert the resulting Hash into an Object" do
|
50
|
+
class TestTransformerWithToObject
|
51
|
+
include Omnis::Transformer
|
52
|
+
property(:ref) {|src| src["ref_anixe"]}
|
53
|
+
def to_object(hash)
|
54
|
+
OpenStruct.new(hash)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
t = TestTransformerWithToObject.new
|
58
|
+
t.transform(doc).should == OpenStruct.new(ref: "1abc")
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'provides a tranformer lambda' do
|
62
|
+
class TestXformer
|
63
|
+
include Omnis::Transformer
|
64
|
+
property(:ref) {|src| src['ref_anixe']}
|
65
|
+
end
|
66
|
+
t = TestXformer.new
|
67
|
+
xformer = t.to_proc
|
68
|
+
xformer.should be_a Proc
|
69
|
+
xformer.({"ref_anixe" => "2two"}).should == {:ref => "2two"}
|
70
|
+
end
|
71
|
+
end
|
data/spec/spec.rake
CHANGED
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: omnis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,72 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bson_ext
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.7.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.7.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: monadic
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: mongo
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.7.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.7.0
|
14
78
|
- !ruby/object:Gem::Dependency
|
15
79
|
name: rspec
|
16
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,7 +156,7 @@ dependencies:
|
|
92
156
|
- !ruby/object:Gem::Version
|
93
157
|
version: '0'
|
94
158
|
- !ruby/object:Gem::Dependency
|
95
|
-
name:
|
159
|
+
name: rake
|
96
160
|
requirement: !ruby/object:Gem::Requirement
|
97
161
|
none: false
|
98
162
|
requirements:
|
@@ -108,22 +172,22 @@ dependencies:
|
|
108
172
|
- !ruby/object:Gem::Version
|
109
173
|
version: '0'
|
110
174
|
- !ruby/object:Gem::Dependency
|
111
|
-
name:
|
175
|
+
name: rb-fsevent
|
112
176
|
requirement: !ruby/object:Gem::Requirement
|
113
177
|
none: false
|
114
178
|
requirements:
|
115
|
-
- -
|
179
|
+
- - ~>
|
116
180
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
181
|
+
version: 0.9.1
|
118
182
|
type: :development
|
119
183
|
prerelease: false
|
120
184
|
version_requirements: !ruby/object:Gem::Requirement
|
121
185
|
none: false
|
122
186
|
requirements:
|
123
|
-
- -
|
187
|
+
- - ~>
|
124
188
|
- !ruby/object:Gem::Version
|
125
|
-
version:
|
126
|
-
description: Helps a read-only ORM kind-of
|
189
|
+
version: 0.9.1
|
190
|
+
description: Helps with a read-only ORM kind-of, more useful than the description
|
127
191
|
email:
|
128
192
|
- pz@anixe.pl
|
129
193
|
executables: []
|
@@ -132,13 +196,24 @@ extra_rdoc_files: []
|
|
132
196
|
files:
|
133
197
|
- .gitignore
|
134
198
|
- Gemfile
|
199
|
+
- Guardfile
|
135
200
|
- LICENSE.txt
|
136
201
|
- README.md
|
137
202
|
- Rakefile
|
138
203
|
- lib/omnis.rb
|
204
|
+
- lib/omnis/mongo_query.rb
|
205
|
+
- lib/omnis/mongo_report.rb
|
206
|
+
- lib/omnis/nested_hash_extractor.rb
|
207
|
+
- lib/omnis/operators.rb
|
208
|
+
- lib/omnis/query.rb
|
209
|
+
- lib/omnis/transformer.rb
|
139
210
|
- lib/omnis/version.rb
|
140
211
|
- omnis.gemspec
|
141
|
-
-
|
212
|
+
- spec/lib/omnis/mongo_query_spec.rb
|
213
|
+
- spec/lib/omnis/nested_hash_extractor_spec.rb
|
214
|
+
- spec/lib/omnis/operators_spec.rb
|
215
|
+
- spec/lib/omnis/query_spec.rb
|
216
|
+
- spec/lib/omnis/transformer_spec.rb
|
142
217
|
- spec/spec.rake
|
143
218
|
- spec/spec_helper.rb
|
144
219
|
homepage: http://github.com/pzol/omnis
|
@@ -153,12 +228,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
228
|
- - ! '>='
|
154
229
|
- !ruby/object:Gem::Version
|
155
230
|
version: '0'
|
231
|
+
segments:
|
232
|
+
- 0
|
233
|
+
hash: -3696760382807505073
|
156
234
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
235
|
none: false
|
158
236
|
requirements:
|
159
237
|
- - ! '>='
|
160
238
|
- !ruby/object:Gem::Version
|
161
239
|
version: '0'
|
240
|
+
segments:
|
241
|
+
- 0
|
242
|
+
hash: -3696760382807505073
|
162
243
|
requirements: []
|
163
244
|
rubyforge_project:
|
164
245
|
rubygems_version: 1.8.23
|
@@ -166,5 +247,10 @@ signing_key:
|
|
166
247
|
specification_version: 3
|
167
248
|
summary: see above
|
168
249
|
test_files:
|
250
|
+
- spec/lib/omnis/mongo_query_spec.rb
|
251
|
+
- spec/lib/omnis/nested_hash_extractor_spec.rb
|
252
|
+
- spec/lib/omnis/operators_spec.rb
|
253
|
+
- spec/lib/omnis/query_spec.rb
|
254
|
+
- spec/lib/omnis/transformer_spec.rb
|
169
255
|
- spec/spec.rake
|
170
256
|
- spec/spec_helper.rb
|
data/readme.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
class BookingQuery
|
2
|
-
include Omnis::MongoQuery
|
3
|
-
|
4
|
-
collection Mongo::Connection.new['bms']['bookings']
|
5
|
-
|
6
|
-
def parse_date(value)
|
7
|
-
case result = Time.parse(value) rescue Chronic.parse(value, :guess => false)
|
8
|
-
when Time; Between.new(m, result.getlocal.beginning_of_day..result.getlocal.end_of_day)
|
9
|
-
when Chronic::Span; Between.new(m, result)
|
10
|
-
else nil
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
param :contract, Matches
|
15
|
-
param :date_from {|value| parse_date(value) }
|
16
|
-
param :date_to {|value| parse_date(value) }
|
17
|
-
|
18
|
-
fields %w[ref_anixe ref_customer status passengers date_status_modified date_from date_to description product contract agency services]
|
19
|
-
end
|
20
|
-
|
21
|
-
class BookingTransformer
|
22
|
-
include Omnis::DataTransformer
|
23
|
-
|
24
|
-
def extract_passenger(doc)
|
25
|
-
->doc { Maybe(doc)['passengers'].map {|v| v.first.values.slice(1..2).join(' ') }.or('Unknown').fetch.to_s }
|
26
|
-
end
|
27
|
-
|
28
|
-
property :ref_anixe, "ref_anixe"
|
29
|
-
property :ref_customer, "ref_customer"
|
30
|
-
property :status, "status"
|
31
|
-
property :passenger, extract_passenger(doc)
|
32
|
-
property :date "date_status_modified", :default => Time.at(0), :format => ->v { v.to_s(:date) }
|
33
|
-
property :description, "description"
|
34
|
-
property :product, "product"
|
35
|
-
property :contract, "contract"
|
36
|
-
property :agency, "agency"
|
37
|
-
property :date_from, "services.0.date_from", :default => "n/a", :format => ->v { v.to_s(:date) }
|
38
|
-
property :date_to, "services.0.date_to", :default => "n/a", :format => ->v { v.to_s(:date) }
|
39
|
-
|
40
|
-
def to_object(doc)
|
41
|
-
OpenStruct.new(doc)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
present BookingQuery.new(params), Foo.new(NestedHashExtractor.new)
|
46
|
-
|
47
|
-
def present(query, transformer)
|
48
|
-
query.call(transformer)
|
49
|
-
end
|