stockboy 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +5 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +12 -0
- data/Guardfile +10 -0
- data/LICENSE +21 -0
- data/README.md +293 -0
- data/Rakefile +30 -0
- data/lib/stockboy.rb +80 -0
- data/lib/stockboy/attribute.rb +11 -0
- data/lib/stockboy/attribute_map.rb +74 -0
- data/lib/stockboy/candidate_record.rb +130 -0
- data/lib/stockboy/configuration.rb +62 -0
- data/lib/stockboy/configurator.rb +176 -0
- data/lib/stockboy/dsl.rb +68 -0
- data/lib/stockboy/exceptions.rb +3 -0
- data/lib/stockboy/filter.rb +58 -0
- data/lib/stockboy/filter_chain.rb +41 -0
- data/lib/stockboy/filters.rb +11 -0
- data/lib/stockboy/filters/missing_email.rb +37 -0
- data/lib/stockboy/job.rb +241 -0
- data/lib/stockboy/mapped_record.rb +59 -0
- data/lib/stockboy/provider.rb +238 -0
- data/lib/stockboy/providers.rb +11 -0
- data/lib/stockboy/providers/file.rb +135 -0
- data/lib/stockboy/providers/ftp.rb +205 -0
- data/lib/stockboy/providers/http.rb +123 -0
- data/lib/stockboy/providers/imap.rb +290 -0
- data/lib/stockboy/providers/soap.rb +120 -0
- data/lib/stockboy/railtie.rb +28 -0
- data/lib/stockboy/reader.rb +59 -0
- data/lib/stockboy/readers.rb +11 -0
- data/lib/stockboy/readers/csv.rb +115 -0
- data/lib/stockboy/readers/fixed_width.rb +121 -0
- data/lib/stockboy/readers/spreadsheet.rb +144 -0
- data/lib/stockboy/readers/xml.rb +155 -0
- data/lib/stockboy/registry.rb +42 -0
- data/lib/stockboy/source_record.rb +43 -0
- data/lib/stockboy/string_pool.rb +35 -0
- data/lib/stockboy/template_file.rb +44 -0
- data/lib/stockboy/translations.rb +70 -0
- data/lib/stockboy/translations/boolean.rb +58 -0
- data/lib/stockboy/translations/date.rb +41 -0
- data/lib/stockboy/translations/decimal.rb +33 -0
- data/lib/stockboy/translations/default_empty_string.rb +38 -0
- data/lib/stockboy/translations/default_false.rb +41 -0
- data/lib/stockboy/translations/default_nil.rb +38 -0
- data/lib/stockboy/translations/default_true.rb +41 -0
- data/lib/stockboy/translations/default_zero.rb +41 -0
- data/lib/stockboy/translations/integer.rb +33 -0
- data/lib/stockboy/translations/string.rb +33 -0
- data/lib/stockboy/translations/time.rb +41 -0
- data/lib/stockboy/translations/uk_date.rb +51 -0
- data/lib/stockboy/translations/us_date.rb +51 -0
- data/lib/stockboy/translator.rb +66 -0
- data/lib/stockboy/version.rb +3 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/fixtures/files/a_garbage.csv +1 -0
- data/spec/fixtures/files/test_data-20120101.csv +1 -0
- data/spec/fixtures/files/test_data-20120202.csv +1 -0
- data/spec/fixtures/files/z_garbage.csv +1 -0
- data/spec/fixtures/jobs/test_job.rb +1 -0
- data/spec/fixtures/soap/get_list/fault.xml +8 -0
- data/spec/fixtures/soap/get_list/success.xml +18 -0
- data/spec/fixtures/spreadsheets/test_data.xls +0 -0
- data/spec/fixtures/spreadsheets/test_row_options.xls +0 -0
- data/spec/fixtures/xml/body.xml +14 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/stockboy/attribute_map_spec.rb +59 -0
- data/spec/stockboy/attribute_spec.rb +11 -0
- data/spec/stockboy/candidate_record_spec.rb +150 -0
- data/spec/stockboy/configuration_spec.rb +28 -0
- data/spec/stockboy/configurator_spec.rb +127 -0
- data/spec/stockboy/filter_chain_spec.rb +40 -0
- data/spec/stockboy/filter_spec.rb +41 -0
- data/spec/stockboy/filters/missing_email_spec.rb +26 -0
- data/spec/stockboy/filters_spec.rb +38 -0
- data/spec/stockboy/job_spec.rb +238 -0
- data/spec/stockboy/mapped_record_spec.rb +30 -0
- data/spec/stockboy/provider_spec.rb +34 -0
- data/spec/stockboy/providers/file_spec.rb +116 -0
- data/spec/stockboy/providers/ftp_spec.rb +143 -0
- data/spec/stockboy/providers/http_spec.rb +94 -0
- data/spec/stockboy/providers/imap_spec.rb +76 -0
- data/spec/stockboy/providers/soap_spec.rb +107 -0
- data/spec/stockboy/providers_spec.rb +38 -0
- data/spec/stockboy/readers/csv_spec.rb +68 -0
- data/spec/stockboy/readers/fixed_width_spec.rb +52 -0
- data/spec/stockboy/readers/spreadsheet_spec.rb +121 -0
- data/spec/stockboy/readers/xml_spec.rb +94 -0
- data/spec/stockboy/readers_spec.rb +30 -0
- data/spec/stockboy/source_record_spec.rb +19 -0
- data/spec/stockboy/template_file_spec.rb +30 -0
- data/spec/stockboy/translations/boolean_spec.rb +48 -0
- data/spec/stockboy/translations/date_spec.rb +38 -0
- data/spec/stockboy/translations/decimal_spec.rb +23 -0
- data/spec/stockboy/translations/default_empty_string_spec.rb +32 -0
- data/spec/stockboy/translations/default_false_spec.rb +25 -0
- data/spec/stockboy/translations/default_nil_spec.rb +32 -0
- data/spec/stockboy/translations/default_true_spec.rb +25 -0
- data/spec/stockboy/translations/default_zero_spec.rb +32 -0
- data/spec/stockboy/translations/integer_spec.rb +22 -0
- data/spec/stockboy/translations/string_spec.rb +22 -0
- data/spec/stockboy/translations/time_spec.rb +27 -0
- data/spec/stockboy/translations/uk_date_spec.rb +37 -0
- data/spec/stockboy/translations/us_date_spec.rb +37 -0
- data/spec/stockboy/translations_spec.rb +55 -0
- data/spec/stockboy/translator_spec.rb +27 -0
- data/stockboy.gemspec +32 -0
- metadata +305 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'stockboy/reader'
|
2
|
+
require 'stockboy/string_pool'
|
3
|
+
|
4
|
+
module Stockboy::Readers
|
5
|
+
|
6
|
+
# Extract data from XML
|
7
|
+
#
|
8
|
+
# This works great with SOAP, probably not fully-featured yet for various XML
|
9
|
+
# formats. The SOAP provider returns a hash, because it takes care of
|
10
|
+
# extracting the envelope body already, so this reader supports options for
|
11
|
+
# reading elements from a nested hash too.
|
12
|
+
#
|
13
|
+
# Backed by the Nori gem from Savon, see nori for full options.
|
14
|
+
#
|
15
|
+
class XML < Stockboy::Reader
|
16
|
+
include Stockboy::StringPool
|
17
|
+
|
18
|
+
# Override source encoding
|
19
|
+
#
|
20
|
+
# @!attribute [rw] encoding
|
21
|
+
# @return [String]
|
22
|
+
#
|
23
|
+
dsl_attr :encoding
|
24
|
+
|
25
|
+
# Element nesting to traverse, the last one should represent the record
|
26
|
+
# instances that contain tags for each attribute.
|
27
|
+
#
|
28
|
+
# @!attribute [rw] elements
|
29
|
+
# @return [Array]
|
30
|
+
# @example
|
31
|
+
# elements ["allItemsResponse", "itemList", "recordItem"]
|
32
|
+
#
|
33
|
+
dsl_attr :elements, attr_accessor: false
|
34
|
+
|
35
|
+
# Removes namespace prefixes from tag names, default true.
|
36
|
+
#
|
37
|
+
# @!attribute [rw] strip_namespaces
|
38
|
+
# @return [Boolean]
|
39
|
+
#
|
40
|
+
dsl_attr :strip_namespaces, attr_accessor: false
|
41
|
+
|
42
|
+
# Change tag formatting, e.g. underscore if it happens to match your actual
|
43
|
+
# record attributes
|
44
|
+
#
|
45
|
+
# @!attribute [rw] convert_tags_to
|
46
|
+
# @return [Proc]
|
47
|
+
# @example
|
48
|
+
# convert_tags_to ->(tag) { tag.underscore }
|
49
|
+
#
|
50
|
+
dsl_attr :convert_tags_to, attr_accessor: false
|
51
|
+
|
52
|
+
# Detects input tag types and tries to extract dates, times, etc. from the data.
|
53
|
+
# Normally this is handled by the attribute map.
|
54
|
+
#
|
55
|
+
# @!attribute [rw] advanced_typecasting
|
56
|
+
# @return [Boolean]
|
57
|
+
#
|
58
|
+
dsl_attr :advanced_typecasting, attr_accessor: false
|
59
|
+
|
60
|
+
# Defaults to Nokogiri. Why would you change it?
|
61
|
+
#
|
62
|
+
# @!attribute [rw] parser
|
63
|
+
# @return [Symbol]
|
64
|
+
#
|
65
|
+
dsl_attr :parser, attr_accessor: false
|
66
|
+
|
67
|
+
[:strip_namespaces, :convert_tags_to, :advanced_typecasting, :parser].each do |opt|
|
68
|
+
define_method(opt) { @xml_options[opt] }
|
69
|
+
define_method(:"#{opt}=") { |value| @xml_options[opt] = value }
|
70
|
+
end
|
71
|
+
|
72
|
+
def elements
|
73
|
+
convert_tags_to ? @elements.map(&convert_tags_to) : @elements
|
74
|
+
end
|
75
|
+
|
76
|
+
def elements=(schema)
|
77
|
+
return @elements = [] unless schema
|
78
|
+
raise(ArgumentError, "expected an array of XML tag strings") unless schema.is_a? Array
|
79
|
+
@elements = schema.map(&:to_s)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!endgroup
|
83
|
+
|
84
|
+
# Initialize a new XML reader
|
85
|
+
#
|
86
|
+
def initialize(opts={}, &block)
|
87
|
+
super
|
88
|
+
self.elements = opts.delete(:elements)
|
89
|
+
@xml_options = opts
|
90
|
+
DSL.new(self).instance_eval(&block) if block_given?
|
91
|
+
end
|
92
|
+
|
93
|
+
# XML options passed to the underlying Nori instance
|
94
|
+
#
|
95
|
+
# @!attribute [r] options
|
96
|
+
# @return [Hash]
|
97
|
+
#
|
98
|
+
def options
|
99
|
+
@xml_options
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse(data)
|
103
|
+
hash = if data.is_a? Hash
|
104
|
+
data
|
105
|
+
else
|
106
|
+
if data.respond_to? :to_xml
|
107
|
+
data.to_xml("UTF-8")
|
108
|
+
nori.parse(data)
|
109
|
+
elsif data.respond_to? :to_hash
|
110
|
+
data.to_hash
|
111
|
+
else
|
112
|
+
data.encode!("UTF-8", encoding) if encoding
|
113
|
+
nori.parse(data)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
with_string_pool do
|
118
|
+
remap_keys hash
|
119
|
+
extract hash
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def nori
|
126
|
+
@nori ||= Nori.new(options)
|
127
|
+
end
|
128
|
+
|
129
|
+
def extract(hash)
|
130
|
+
result = elements.inject hash do |memo, key|
|
131
|
+
return [] if memo[key].nil?
|
132
|
+
memo[key]
|
133
|
+
end
|
134
|
+
|
135
|
+
result = [result] unless result.is_a? Array
|
136
|
+
result.compact!
|
137
|
+
result
|
138
|
+
end
|
139
|
+
|
140
|
+
def remap_keys(node)
|
141
|
+
mapper = convert_tags_to || ->(tag) { tag }
|
142
|
+
case node
|
143
|
+
when Hash
|
144
|
+
node.keys.each do |k|
|
145
|
+
tag = string_pool(mapper.call(k))
|
146
|
+
node[tag] = remap_keys(node.delete(k))
|
147
|
+
end
|
148
|
+
when Array
|
149
|
+
node.each { |value| remap_keys(value) }
|
150
|
+
end
|
151
|
+
node
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Stockboy
|
2
|
+
|
3
|
+
# Holds a collection of registered classes for convenient reference by
|
4
|
+
# symbolic name
|
5
|
+
#
|
6
|
+
module Registry
|
7
|
+
|
8
|
+
def self.extended(base)
|
9
|
+
base.class_eval do
|
10
|
+
@registry = {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Register a class under a convenient symbolic name
|
15
|
+
#
|
16
|
+
# @param [Symbol] key Symbolic name of the class
|
17
|
+
# @param [Class] provider Class to be returned when requested
|
18
|
+
#
|
19
|
+
def register(key, provider)
|
20
|
+
@registry[key] = provider
|
21
|
+
end
|
22
|
+
|
23
|
+
# Look up a class and return it by symbolic name
|
24
|
+
#
|
25
|
+
# @param [Symbol] key
|
26
|
+
# @return [Class]
|
27
|
+
#
|
28
|
+
def find(key)
|
29
|
+
@registry[key]
|
30
|
+
end
|
31
|
+
alias_method :[], :find
|
32
|
+
|
33
|
+
# List all named classes in the registry
|
34
|
+
#
|
35
|
+
# @return [Hash]
|
36
|
+
#
|
37
|
+
def all
|
38
|
+
@registry
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'stockboy/mapped_record'
|
2
|
+
|
3
|
+
module Stockboy
|
4
|
+
|
5
|
+
# This represents the raw "input" side of a {CandidateRecord}
|
6
|
+
#
|
7
|
+
# It provides access to the original field values before mapping or
|
8
|
+
# translation as hash keys.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# input = SourceRecord.new(
|
12
|
+
# {check_in: "2012-12-12"},
|
13
|
+
# {"RawCheckIn" => "2012-12-12"})
|
14
|
+
#
|
15
|
+
# input["RawCheckIn"] # => "2012-12-12"
|
16
|
+
# input.check_in # => "2012-12-12"
|
17
|
+
#
|
18
|
+
class SourceRecord < MappedRecord
|
19
|
+
|
20
|
+
# Initialize a new instance
|
21
|
+
#
|
22
|
+
# @param [Hash{Symbol=>Object}] mapped_fields
|
23
|
+
# Represents the raw values mapped to the final attribute names
|
24
|
+
# @param [Hash] data_fields
|
25
|
+
# The raw input fields with original key values
|
26
|
+
#
|
27
|
+
def initialize(mapped_fields, data_fields)
|
28
|
+
@data_fields = data_fields
|
29
|
+
super(mapped_fields)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Access a raw field value by the original input field name
|
33
|
+
#
|
34
|
+
# @param [String] key
|
35
|
+
#
|
36
|
+
def [](key)
|
37
|
+
key = key.to_s if key.is_a? Symbol
|
38
|
+
@data_fields[key]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Stockboy
|
2
|
+
|
3
|
+
# Holds frozen strings for shared lookup between different object instances
|
4
|
+
#
|
5
|
+
# @visibility private
|
6
|
+
#
|
7
|
+
module StringPool
|
8
|
+
|
9
|
+
# Pass a block to yield a new string pool context around a group of
|
10
|
+
# actions that should share the same string key instances
|
11
|
+
#
|
12
|
+
# @yield
|
13
|
+
#
|
14
|
+
def with_string_pool
|
15
|
+
@string_pool = []
|
16
|
+
result = yield
|
17
|
+
@string_pool = []
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
# Look up duplicate strings and return the shared frozen string
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
#
|
25
|
+
def string_pool(name)
|
26
|
+
if i = @string_pool.index(name)
|
27
|
+
@string_pool[i]
|
28
|
+
else
|
29
|
+
@string_pool << name.freeze
|
30
|
+
name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'stockboy/configuration'
|
2
|
+
|
3
|
+
module Stockboy
|
4
|
+
|
5
|
+
# Find and read template files from the configured load paths
|
6
|
+
#
|
7
|
+
module TemplateFile
|
8
|
+
|
9
|
+
# Read template file contents for defining a new job
|
10
|
+
#
|
11
|
+
# @param [String] template_name
|
12
|
+
# The file basename of a predefined template
|
13
|
+
# @return [String] Job template DSL or nil if nothing is found
|
14
|
+
#
|
15
|
+
def self.read(template_name)
|
16
|
+
return template_name.read if template_name.is_a? File
|
17
|
+
return unless path = find(template_name)
|
18
|
+
|
19
|
+
File.read(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find a named DSL template from configuration.template_load_paths
|
23
|
+
#
|
24
|
+
# @param [String] filename Template basename to be searched from load paths
|
25
|
+
# @return [String] The full path to the first matched filename if found
|
26
|
+
#
|
27
|
+
def self.find(filename)
|
28
|
+
sources = template_file_paths(filename)
|
29
|
+
Dir.glob(sources).first
|
30
|
+
end
|
31
|
+
|
32
|
+
# Potential locations for finding a template file
|
33
|
+
#
|
34
|
+
# @param [String] filename Template basename
|
35
|
+
# @return [Array] filename on each possible load path
|
36
|
+
#
|
37
|
+
def self.template_file_paths(filename)
|
38
|
+
filename = "#{filename}.rb" unless filename =~ /\.rb$/
|
39
|
+
load_paths = Array(Stockboy.configuration.template_load_paths)
|
40
|
+
load_paths.map { |d| File.join(d, filename) }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'stockboy/exceptions'
|
2
|
+
require 'stockboy/translator'
|
3
|
+
|
4
|
+
module Stockboy
|
5
|
+
|
6
|
+
# Registry of available {Translator} classes for lookup by symbolic name in the
|
7
|
+
# job template DSL.
|
8
|
+
#
|
9
|
+
module Translations
|
10
|
+
|
11
|
+
@registry ||= {}
|
12
|
+
|
13
|
+
# Register a translator under a convenient symbolic name
|
14
|
+
#
|
15
|
+
# @param [Symbol] name
|
16
|
+
# Symbolic name of the class
|
17
|
+
# @param [Translator, #call] callable
|
18
|
+
# Translator class or any callable object
|
19
|
+
#
|
20
|
+
def self.register(name, callable)
|
21
|
+
if callable.respond_to?(:call) or callable < Stockboy::Translator
|
22
|
+
@registry[name.to_sym] = callable
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Registered translators must be callable"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Calls a named translator for the raw value
|
29
|
+
#
|
30
|
+
# @param [Symbol, Translator, #call] func_name
|
31
|
+
# Symbol representing a registered translator, or an actual translator
|
32
|
+
# @param [SourceRecord, MappedRecord, Hash, String] context
|
33
|
+
# Collection of fields or the raw value to which the translation is applied
|
34
|
+
#
|
35
|
+
def self.translate(func_name, context)
|
36
|
+
translator_for(:value, func_name).call(context)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Prepare a translator for a given attribute
|
40
|
+
#
|
41
|
+
# @param [Symbol] attr
|
42
|
+
# Name of the mapped record attribute to address for translation
|
43
|
+
# @param [Symbol, #call] lookup
|
44
|
+
# Symbolic translator name or callable object
|
45
|
+
# @return [Translator] instance
|
46
|
+
#
|
47
|
+
def self.translator_for(attr, lookup)
|
48
|
+
if lookup.respond_to?(:call)
|
49
|
+
lookup
|
50
|
+
elsif tr = self[lookup]
|
51
|
+
tr.is_a?(Class) && tr < Stockboy::Translator ? tr.new(attr) : tr
|
52
|
+
else
|
53
|
+
->(context) { context.public_send attr } # no-op
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Look up a translation and return it by symbolic name
|
58
|
+
#
|
59
|
+
# @param [Symbol] func_name
|
60
|
+
# @return [Translator]
|
61
|
+
#
|
62
|
+
def self.find(func_name)
|
63
|
+
@registry[func_name]
|
64
|
+
end
|
65
|
+
class << self
|
66
|
+
alias_method :[], :find
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'stockboy/translator'
|
2
|
+
|
3
|
+
module Stockboy::Translations
|
4
|
+
|
5
|
+
# Convert common false-like and true-like values to proper boolean +true+ or
|
6
|
+
# +false+.
|
7
|
+
#
|
8
|
+
# Returns nil for indeterminate values. This should be chained with a
|
9
|
+
# default value translator like [DefaultFalse] or [DefaultTrue].
|
10
|
+
#
|
11
|
+
# == Job template DSL
|
12
|
+
#
|
13
|
+
# Registered as +:boolean+. Use with:
|
14
|
+
#
|
15
|
+
# attributes do
|
16
|
+
# active as: :boolean
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# bool = Stockboy::Translator::Boolean.new
|
21
|
+
#
|
22
|
+
# record.active = 't'
|
23
|
+
# bool.translate(record, :active) # => true
|
24
|
+
#
|
25
|
+
# record.active = 'f'
|
26
|
+
# bool.translate(record, :active) # => false
|
27
|
+
#
|
28
|
+
# record.active = '1'
|
29
|
+
# bool.translate(record, :active) # => true
|
30
|
+
#
|
31
|
+
# record.active = '0'
|
32
|
+
# bool.translate(record, :active) # => false
|
33
|
+
#
|
34
|
+
# record.active = 'y'
|
35
|
+
# bool.translate(record, :active) # => true
|
36
|
+
#
|
37
|
+
# record.active = 'n'
|
38
|
+
# bool.translate(record, :active) # => false
|
39
|
+
#
|
40
|
+
# record.active = '?'
|
41
|
+
# bool.translate(record, :active) # => nil
|
42
|
+
#
|
43
|
+
class Boolean < Stockboy::Translator
|
44
|
+
TRUTHY_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'y', 'Y', 'yes', 'YES']
|
45
|
+
FALSY_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'n', 'N', 'no', 'NO']
|
46
|
+
|
47
|
+
# @return [Boolean]
|
48
|
+
#
|
49
|
+
def translate(context)
|
50
|
+
value = field_value(context, field_key)
|
51
|
+
|
52
|
+
return true if TRUTHY_VALUES.include?(value)
|
53
|
+
return false if FALSY_VALUES.include?(value)
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|