Closed olistik closed 3 years ago
The goal to achieve is to implement an easy to use readable tag for the end user that will be mapped to a decorated field.
For example let's say we have a Subscription
model related to a Participant
and this tag to replace {Subscription_date} {Participant_first_name}
We want to print the subscription contract given the Subscription
instance
in this scenario we have the Subscription
@instance
so we'd need to replace as:
@instance.date
@instance.participant.first_name
We want to print the participant details given the Participant
instance
@instance.first_name
Notice the difference here we don't need to call participant
relation as we already have his instance
@xkraty Following our call, one idea that could help us be flexible is to pass an object that responds to call(key:)
that is invoked for each occurrence found:
diff --git a/lib/doc2pdf.rb b/lib/doc2pdf.rb
index b3811c4..aea28d0 100644
--- a/lib/doc2pdf.rb
+++ b/lib/doc2pdf.rb
@@ -18,20 +18,20 @@ module Doc2pdf
end.flatten
end
- def self.replace!(document:)
+ def self.replace!(document:, replacer:)
DocumentTraversal.new(document: document).each do |item|
search(item.text).each do |occurrence|
- item.substitute(occurrence, "QUALCOSA")
+ item.substitute(occurrence, replacer.call(key: occurrence))
end
end
document
end
- def self.replace_and_save!(document:, output_base_path:)
+ def self.replace_and_save!(document:, output_base_path:, replacer:)
FileUtils.mkdir_p(File.dirname(output_base_path))
- replace!(document: document)
+ replace!(document: document, replacer: replacer)
doc_path = "#{output_base_path}.doc"
pdf_path = "#{output_base_path}.pdf"
and this allows us to do something like this:
Define a simple exception:
class KeyNotFound < StandardError; end
Some data to play with:
participant = Struct.new(:first_name, :birthplace).new('Maurizio', 'Sesto San Giovanni')
subscription_contract = Struct.new(:contract_name, :participant).new('Fornitura energia elettrica', participant)
subscription_contract.contract_name # => 'Fornitura energia elettrica'
subscription_contract.participant.first_name # => 'Maurizio'
subscription_contract.participant.birthplace # => 'Sesto San Giovanni'
The suggested approach. ^_^
class MyReplacer
def initialize(subscription_contract:)
self.subscription_contract = subscription_contract
end
def call(key:)
case key
when 'nome' then subscription_contract.contract_name
when 'nome_partecipante' then subscription_contract.participant.first_name
when 'luogo_nascita_partecipante' then subscription_contract.participant.birthplace
else
raise KeyNotFound.new(key: key)
end
end
private
attr_accessor :subscription_contract
end
replacer.call(key: 'nome') # => 'Fornitura energia elettrica'
replacer.call(key: 'nome_partecipante') # => 'Maurizio'
replacer.call(key: 'luogo_nascita_partecipante') # => 'Sesto San Giovanni'
replacer.call(key: 'wat') # => KeyNotFound ({:key=>"wat", :replacer_class=>MyReplacer})
A simple mapping can be implemented with proper method names (the overrides).
class MyReplacer
def initialize(subscription_contract:)
self.subscription_contract = subscription_contract
end
def call(key:)
send(key)
end
private
def contract_name
subscription_contract.contract_name
end
def method_missing(m, *args, &block)
m.to_s.split('.').each_with_object([subscription_contract]) do |method_name, memo|
memo[0] = memo[0].send(method_name)
end[0]
end
attr_accessor :subscription_contract
end
replacer = MyReplacer.new(subscription_contract: subscription_contract)
replacer.call(key: 'contract_name') # => 'Fornitura energia elettrica'
replacer.call(key: 'participant.first_name') # => 'Maurizio'
replacer.call(key: 'participant.birthplace') # => 'Sesto San Giovanni'
class MyReplacer
MAPPING = {
'nome' => 'contract_name',
'nome_partecipante' => 'participant.first_name',
'luogo_nascita_partecipante' => 'participant.birthplace',
}.freeze
def initialize(subscription_contract:)
self.subscription_contract = subscription_contract
end
def call(key:)
unless MAPPING.keys.include?(key)
raise KeyNotFound.new(key: key, replacer_class: self.class)
end
send(MAPPING[key])
end
private
def ciccio
'pasticcio'
end
def method_missing(m, *args, &block)
m.to_s.split('.').each_with_object([subscription_contract]) do |method_name, memo|
memo[0] = memo[0].send(method_name)
end[0]
end
attr_accessor :subscription_contract
end
replacer = MyReplacer.new(subscription_contract: subscription_contract)
replacer.call(key: 'nome') # => 'Fornitura energia elettrica'
replacer.call(key: 'nome_partecipante') # => 'Maurizio'
replacer.call(key: 'luogo_nascita_partecipante') # => 'Sesto San Giovanni'
replacer.call(key: 'ciccio') # => 'pasticcio'
replacer.call(key: 'wat') # => KeyNotFound ({:key=>"wat", :replacer_class=>MyReplacer})
It would be interesting to compare performance of the first and third solution, I mean I like how clean the first approach is and also I like how would be straightforward the usage of the third solution with just a simple mapping but with the ability to extend.
@xkraty
It would be interesting to compare performance of the first and third solution, I mean I like how clean the first approach is and also I like how would be straightforward the usage of the third solution with just a simple mapping but with the ability to extend.
They're not mutually exclusive. Regarding this gem, we just have to implement this patch:
diff --git a/lib/doc2pdf.rb b/lib/doc2pdf.rb
index b3811c4..aea28d0 100644
--- a/lib/doc2pdf.rb
+++ b/lib/doc2pdf.rb
@@ -18,20 +18,20 @@ module Doc2pdf
end.flatten
end
- def self.replace!(document:)
+ def self.replace!(document:, replacer:)
DocumentTraversal.new(document: document).each do |item|
search(item.text).each do |occurrence|
- item.substitute(occurrence, "QUALCOSA")
+ item.substitute(occurrence, replacer.call(key: occurrence))
end
end
document
end
- def self.replace_and_save!(document:, output_base_path:)
+ def self.replace_and_save!(document:, output_base_path:, replacer:)
FileUtils.mkdir_p(File.dirname(output_base_path))
- replace!(document: document)
+ replace!(document: document, replacer: replacer)
doc_path = "#{output_base_path}.doc"
pdf_path = "#{output_base_path}.pdf"
And let the consumers decide the approach they like the most, using hash maps, method missing and whatnot. :-)
I do agree with you, we could ship the gem with a ready to use method missing solution or DIY, I like it!
@xkraty
I do agree with you, we could ship the gem with a ready to use method missing solution or DIY, I like it!
I'd just provide the simplest possible implementation in the README.md
and let the users roll their own:
replacer = lambda ->(key:) { "#{key}_foo" }
# or
class MyReplacer
def call(key:)
"#{key}_foo"
end
end
replacer = MyReplacer.new
# and then
Doc2pdf.replace!(document: document: replacer: replacer)
Doc2pdf.replace_and_save!(document: document, replacer: replacer, output_base_path: './tmp')
What do you think?
Sounds great!
@xkraty We can ship this replacer:
class SimpleReplacer
def initialize(resource)
@resource = resource
end
def call(key:)
send(key)
end
private
def method_missing(m, *args, &block)
m.to_s.split('.').inject(@resource) do |memo, method_name|
memo.send(method_name)
end
end
end
replacer = SimpleReplacer.new('Foo')
replacer.call(key: 'length.class.to_s.length') # => 7
replacer.call(key: 'asd') # => NoMethodError (undefined method `asd' for "Foo":String)
Along with the warning:
With great powers, comes great responsibility.
As, for one, a typo in the definition of method_missing
ends up with a SystemStackError (stack level too deep)
exception. 😅
So this is without the defined mapping? It just rely on resource methods?
TOOD: define some simple examples to show the expected behaviour of this feature.