Dynamically set parameters in define_method - ruby

I have a class method ::add_method(name, params = {}) that creates an instance method with define_method.
I need the parameters of the defined method to be keyword arguments depending on the params.
class Whatever
def self.add_method(name, params = {})
# do something with params
define_method name do |?|
# some business
end
end
end
The goal is that when the ::add_method is called with:
params = {
foo: { required: false, default: 0 },
bar: { required: true }
}
Whatever.add_method(:hello, params)
then it creates this method:
def hello(foo: 0, bar:)
# some business
end
Nota bene: this is not the real business, I've over simplified it so the question is easier to understand.

So as advised I went to class_eval.
class Whatever
class << self
def add_method(name, parameters = {})
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}(#{method_parameters(parameters)})
#{method_body(parameters)}
end
RUBY
end
# method_parameters({
# foo: { required: false, default: 0 },
# bar: { required: true }
# })
# => "foo: 0, bar:"
def method_parameters(parameters)
parameters.map do |key, options|
value = options[:required] ? '' : " #{options[:default] || 'nil'}"
"#{key}:#{value}"
end.join(', ')
end
# method_parameters({
# foo: { required: false, default: 0 },
# bar: { required: true }
# })
# => "[foo, bar]"
def method_body(parameters)
"[#{parameters.keys.map(&:to_s).join(', ')}]"
end
end
end
params = {
foo: { required: false, default: 0 },
bar: { required: true }
}
Whatever.add_method(:hello, params)
Whatever.new.hello(bar: true) # => [0, true]
Whatever.new.hello(foo: 42, bar: true) # => [42, true]
Whatever.new.hello # missing keyword: bar (ArgumentError)

Related

Ruby transform hash based on mapping

Let's say I have a mapping of how I want a hash to turn out, along with new key names like this:
JSON_MAP = {
image: {
id: :id,
media_url: :url,
time: :duration,
timestamp: :time_posted,
text_caption: :caption,
metadata: {
camera: :camera_type,
flash: :camera_flash
}
},
viewers: {
views: :view_count,
likes: :likes_count
}
}
and I have a hash like this:
{
image: {
id: 1,
media_url: 'http://placekitten.com',
nsfw: false,
time: 4,
timestamp: 14149292,
text_caption: "I'm a kitten",
metadata: {
camera: 'iPhone',
flash: true
}
},
viewers: {
views: 50,
likes: 15
},
extras: {
features: {
enabled: true
}
}
}
I only want it to transform the data so it ends up like:
{
image: {
id: 1,
url: 'http://placekitten.com',
duration: 4,
time_posted: 14149292,
caption: "I'm a kitten",
metadata: {
camera: 'iPhone',
flash: true
}
},
viewers: {
view_count: 50,
likes_count: 15
}
}
Basically, renaming all the keys based on the source map, and deleting any keys that don't match the source map...
You can obtain your desired result using recursion.
def convert(mapper, hsh)
mapper.each_with_object({}) do |(k,o),h|
next unless hsh.key?(k)
if o.is_a? Hash
h[k] = convert(o, hsh[k])
else
h[o] = hsh[k]
end
end
end
Assuming h equals your second hash,
convert(JSON_MAP, h)
#=> { :image=>{
# :id=>1,
# :url=>"http://placekitten.com",
# :duration=>4,
# :time_posted=>14149292,
# :caption=>"I'm a kitten",
# :metadata=>{
# :camera_type=>"iPhone",
# :camera_flash=>true
# }
# },
# :viewers=>{
# :view_count=>50,
# :likes_count=>15
# }
# }

Relay re-fetching failed... error Relay was unable to reconcile edges on a connection

Help, plz! Perhaps someone already faced with my problem....
When re-fetch data with the updated variables -- using relay.setVariables() -- I get the error "Relay was unable to reconcile edges on a connection. This most likely occurred while trying to handle a server response that includes connection edges with nodes that lack anid field." and Relay storage is not updated although the data come correct.
See code below... (Ruby && ES6)
gemfile
gem 'graphql', '0.16.0'
gem 'graphql-relay', '0.11.2'
....server-side code
node_identification.rb
NodeIdentification = GraphQL::Relay::GlobalNodeIdentification.define do
object_from_id -> (id, ctx) do
type, id = NodeIdentification.from_global_id(id)
case type
when 'FrontApp'
Relay::FrontApp::STATIC
else
Object.const_get(type).find_by(id: id)
end
end
type_from_object -> (obj) do
begin
MODEL_TO_TYPE[obj.class.name.to_sym].constantize
rescue
(obj.class.name + 'Type').constantize
end
end
end
MODEL_TO_TYPE = {
:'Relay::FrontApp' => 'FrontAppType'
}
front_app_query_type.rb
FrontAppQueryType = GraphQL::ObjectType.define do
name 'FrontAppRootQuery'
field :node, field: NodeIdentification.field
field :main, FrontAppType do
resolve -> (obj, args, ctx) {
Relay::FrontApp::STATIC
}
end
end
front_app_type.rb
FrontAppType = GraphQL::ObjectType.define do
name 'FrontApp'
# field :node, field: NodeIdentification.field
interfaces [NodeIdentification.interface]
global_id_field :id
field :tips, TipsType do
argument :filters, types.String
resolve -> (obj, args, ctx) {
filters = args[:filters]
begin
filters = JSON.parse(filters).deep_symbolize_keys!
rescue
filters = nil
end
ctx[:filters] = filters
Relay::Tips::STATIC
}
end
connection :footer, FooterMenuItemType.connection_type do
argument :id, types.ID!
resolve ->(obj, args, ctx){
::Footer.order(:id)
}
end
end
tips_connection_type.rb
TipsConnectionType = TipShowType.define_connection do
field :totalCount, types.Int do
resolve -> (obj, args, ctx) {
obj.object.size
}
end
end
TipsType = GraphQL::ObjectType.define do
name 'Tips'
description 'Tips list for home page'
interfaces [NodeIdentification.interface]
global_id_field :id
connection :mostRecent, TipsConnectionType do
argument :limit, types.Int
resolve ->(obj, args, ctx){
tips = ::Tip.active_users.started(Time.zone.now.in_time_zone(ctx[:current_user] ? ctx[:current_user].time_zone : ::User.get_locally_time_zone).to_date).ready.active.moderated.published.includes(:comments, :tip_type).order('created_at desc').limit(args[:limit])
TipHelpers::Filter.filter(tips: tips, filters: ctx[:filters], reorder: 'created_at desc')
}
end
connection :mostPopular, TipsConnectionType do
argument :limit, types.Int
resolve ->(obj, args, ctx){
tips = ::Tip.active_users.started(Time.zone.now.in_time_zone(ctx[:current_user] ? ctx[:current_user].time_zone : ::User.get_locally_time_zone).to_date).ready.active.moderated.published.includes(:comments, :tip_type).order('(SELECT COUNT(*) FROM comments WHERE tid = tips.id) desc').limit(args[:limit])
TipHelpers::Filter.filter(tips: tips, filters: ctx[:filters], reorder: '(SELECT COUNT(*) FROM comments WHERE tid = tips.id) desc')
}
end
end
/app/models/relay/front_app.rb
module Relay
class FrontApp < Struct.new :id
# HACK::// For relay root queries
STATIC = new(id: 'main').freeze
def initialize *args
opts = args.last.is_a?(Hash) ? args.pop : Hash.new
super *args
opts.each_pair do |k, v|
self.send "#{k}=", v
end
end
def self.find(_)
STATIC
end
end
end
** /app/models/relay/tips.rb**
module Relay
class Tips < Struct.new :id
# HACK:// For relay root queries
STATIC = new(id: 'tips').freeze
def initialize *args
opts = args.last.is_a?(Hash) ? args.pop : Hash.new
super *args
opts.each_pair do |k, v|
self.send "#{k}=", v
end
end
def self.find(_)
STATIC
end
end
end
....and client-side code
class MainApp extends React.Component {
constructor(props) {
super(props);
this.state = {
filters: filtersTemplate
};
this.setFilter = this.setFilter.bind(this);
}
setFilter(filter, value, e) {
if (e) {
e.nativeEvent.stopImmediatePropagation();
e.preventDefault();
}
let { filters } = this.state;
if (['currency', 'sum'].inArray(filter)) {
filters.funds[filter] = value;
} else {
filters[filter] = value;
}
this.setState({
filters: filters
});
this.props.relay.setVariables({filters: filters});
}
render() {
let { tips } = this.props.main;
let renderTipsSection = (section) => {
let tipsCount = tips[section] ? tips[section].edges.length : 0;
let blankCount = 10 - tipsCount;
return (
<ul className="tips__list">
{
tips[section] && tips[section].edges.map(({node}) => (
<li key={node.id} className="tips__list_item">
<TipCard node={node}/>
</li>
))
}
{
[...new Array(blankCount).keys()].map((item, idx) => (
<li key={idx} className="tips__list_item">
<TipCard dummy={true}/>
</li>
))
}
</ul>
);
};
return (
<div>
<div className="wrapper">
<div className="TipsWrapper">
<div className="wrapper">
{ renderTipsSection('mostRecent') }
{ renderTipsSection('mostPopular') }
</div>
</div>
</div>
</div>
);
}}
export default Relay.createContainer(MainApp, {
initialVariables: {
filters: {
category: null,
funds: {
currency: null,
sum: null
},
date: null,
location: null,
browse: null
}
},
prepareVariables: (prevVars) => {
return {
...prevVars,
filters: JSON.stringify(prevVars.filters)
}
},
fragments: {
main: () => Relay.QL`
fragment on FrontApp {
tips(filters: $filters) {
mostRecent(first: 10, limit: 10){
edges {
node {
id
tid
title_name
category
}
}
}
mostPopular(first: 10, limit: 10){
edges {
node {
id
tid
title_name
category
}
}
}
}
}
`
}});
When setFilter() triggered the relay.setVariables will be called...
and result...
[RELAY-NETWORK] Run query q3 Object {relayReqId: "q3", relayReqObj: RelayQueryRequest, relayReqType: "query", method: "POST", headers: Object…}
[RELAY-NETWORK] query q3: 3429ms
Warning: Relay was unable to reconcile edges on a connection. This most likely occurred while trying to handle a server response that includes connection edges with nodes that lack an `id` field
After many hours of torment solution of the problem above described was found... see below
SOLUTION
client-side mutation
export default class ApplyFiltersMutation extends Relay.Mutation {
static fragments = {
tips: () => Relay.QL`
fragment on Tips {
id
}
`,
};
getMutation() {
return Relay.QL`mutation {
applyFilters
}`;
}
getVariables() {
return {
filters: this.props.filters
};
}
getFatQuery() {
return Relay.QL`
fragment on ApplyFiltersPayload {
tips
}
`;
}
getConfigs() {
return [
{
type: 'FIELDS_CHANGE',
fieldIDs: {tips: this.props.tips.id},
}
];
}
}
server-side
home_mutations.rb
module HomeMutations
ApplyFilters = GraphQL::Relay::Mutation.define do
name 'ApplyFilters'
input_field :filters, !types.String
return_field :tips, BipsType
resolve -> (args, ctx) {
filters = args[:filters]
begin
filters = JSON.parse(filters).deep_symbolize_keys!
rescue
filters = nil
end
ctx[:filters] = filters
{
tips: Relay::Tips::STATIC
}
}
end
end
tips_type.rb
include TipHelpers::Filter
TipsType = GraphQL::ObjectType.define do
name 'Tips'
description 'Tips list for home page'
interfaces [NodeIdentification.interface]
global_id_field :id
connection :almostRaised, TipsConnectionType do
resolve ->(obj, args, ctx){
TipHelpers::Filter.filter(section: 'almost_raised', filters: ctx[:filters], current_user: ctx[:current_user])
}
end
...
end
end
lib/tip_helpers.rb
class TipHelpers
module Filter
def filter(section:, filters:, current_user:)
...
# p tips.reorder(reorder).to_sql
tips.reorder(reorder)
end
end
end

iterate through nested hashes using conditionals in Ruby

I am trying to build a new_hash from this hash:
languages = {
:oo => {
:ruby => {
:type => "interpreted"
},
:javascript => {
:type => "interpreted"
},
:python => {
:type => "interpreted"
}
},
:functional => {
:clojure => {
:type => "compiled"
},
:erlang => {
:type => "compiled"
},
:javascript => {
:type => "interpreted"
}
}
}
and the desired result is:
{
:ruby => {
:type => "interpreted",
:style => [:oo]
},
:javascript => {
:type => "interpreted",
:style => [:oo, :functional]
},
:python => {
:type => "interpreted",
:style => [:oo]
},
:clojure => {
:type => "compiled",
:style => [:functional]
},
:erlang => {
:type => "compiled",
:style => [:functional]
}
}
Here is what I've done so far:
def reformat_languages(languages)
new_hash = {}
languages.each do |k, v|
v.each do |k1, v1|
new_hash[k1] = v1
new_hash[k1][:style] = []
new_hash[k1][:style] << k
end
end
new_hash
end
unfortunately, I cannot get the desired result. I understand that when the iteration arrives at the second javascript key, it re-writes over the first iteration giving me:
:javascript => {
:type => "interpreted",
:style => [:functional]
}
instead of:
:javascript => {
:type => "interpreted",
:style => [:oo, :functional]
}
Here is a link of a repl.it where I you can see the code in action: https://repl.it/BebC
I know I need to use a conditional, but I am not really sure where and on what to use it. If somebody could help me getting the desired result and explain a little bit why it works the way it works.
You can use something like
h = {}
languages.each do |k, v| # oo or func
v.each do |k1, v1| # ruby/python
if h[k1]
h[k1][:style] << k
else
h[k1] = {type: v1[:type], style: [k]}
end
end
end
It checks to see that h is defined, and if so, appends to its array. Otherwise it defines the entire hash with your type and a style array of size 1.
There is too much unconditional overwriting going on in your code. Should be something like this instead:
new_hash[k1] ||= {} # init to empty hash
new_hash[k1][:type] = v1[:type]
new_hash[k1][:style] ||= [] # make sure array exists
new_hash[k1][:style] << k
Instead of replacing entire new_hash[k1], you should change individual parts of it.
This is not an answer (so please no upvotes). Rather, it is an extended comment to help you understand the code #Martin suggested. (I see you are new to SO and quite possibly to Ruby as well.) Salting code with puts statements, as I have done, is often quite helpful, even after you become experienced with the language.
languages = {
:oo => {
:ruby => {
:type => "interpreted"
},
:javascript => {
:type => "interpreted"
}
},
:functional => {
:clojure => {
:type => "compiled"
},
:javascript => {
:type => "interpreted"
}
}
}
h = {}
languages.each do |k, v| # oo or func
puts "k=#{k}, v=#{v}"
v.each do |k1, v1| # ruby/python
puts " k1=#{k1}, v1=#{v1}"
if h[k1]
puts " h[#{k1}]=#{h[k1]} (truthy)"
h[k1][:style] << k
puts " h after h[#{k1}][:style] << #{k}: #{h}"
else
puts " h[#{k1}].nil?=true (falsy)"
h[k1] = {type: v1[:type], style: [k]}
puts " h after h[#{k1}] = {type: v1[:type], style: #{k}}: #{h}"
end
end
end
prints:
k=oo, v={:ruby=>{:type=>"interpreted"}, :javascript=>{:type=>"interpreted"}}
k1=ruby, v1={:type=>"interpreted"}
h[ruby].nil?=true (falsy)
h after h[ruby] = {type: v1[:type], :style: oo}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]}}
k1=javascript, v1={:type=>"interpreted"}
h[javascript].nil?=true (falsy)
h after h[javascript] = {type: v1[:type], :style: oo}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo]}}
k=functional, v={:clojure=>{:type=>"compiled"}, :javascript=>{:type=>"interpreted"}}
k1=clojure, v1={:type=>"compiled"}
h[clojure].nil?=true (falsy)
h after h[clojure] = {type: v1[:type], :style: functional}:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo]},
:clojure=>{:type=>"compiled", :style=>[:functional]}}
k1=javascript, v1={:type=>"interpreted"}
h[javascript]={:type=>"interpreted", :style=>[:oo]} (truthy)
h after h[javascript][:style] << functional:
{:ruby=>{:type=>"interpreted", :style=>[:oo]},
:javascript=>{:type=>"interpreted", :style=>[:oo, :functional]},
:clojure=>{:type=>"compiled", :style=>[:functional]}}
and returns:
#=> {:oo =>{:ruby=>{:type=>"interpreted"},
# :javascript=>{:type=>"interpreted"}},
# :functional=>{:clojure=>{:type=>"compiled"},
# :javascript=>{:type=>"interpreted"}}}
You are overwriting the hashes generated which is leading to the unexpected behavior you mentioned. The following piece of code does what you need. Its just a slightly modified version of your code.
def reformat_languages(languages)
new_hash = {}
languages.each do |k, v|
v.each do |k1, v1|
new_hash[k1] ||= v1 #ensures we do not overwrite the already generated language hash
new_hash[k1][:style] ||= [] #protecting against re-initialization of the style array
new_hash[k1][:style] << k
end
end
new_hash
end

Populating a hash from an array

I have this array:
params[:types] = [type1, type2, type3...]
I would like to populate my hash the following way using the above array:
params[:hash] = {
"type1" => {
something: something
},
"type2" => {
something: something
},
}
Using a for loop like for index in i ...params[:types] just populates the hash with the last value in the array.
You can use the each_with_object method to do this:
params = {}
params[:types] = ["type1", "type2", "type3"]
params[:types].each_with_object({}) { |k, h| h[k] = { "something" => "something" } }
That last line will return:
=> {"type1"=>{"something"=>"something"}, "type2"=>{"something"=>"something"}, "type3"=>{"something"=>"something"}}
Here is a code snippet example that does what you need.
hash = {}
array.each do |a|
hash[a.to_s] = { "something" => "something" }
end
output:
hash
=> {
"type1" => {
"something" => "something"
},
"type2" => {
"something" => "something"
},
"type3" => {
"something" => "something"
}
}
You could do this:
params = { types: ["type1", "type2", "type3"] }
Hash[params[:types].product([{"something" => "something"}])]
#=> {"type1"=>{"something"=>"something"},
# "type2"=>{"something"=>"something"},
# "type3"=>{"something"=>"something"}}
or with Ruby 2.1,
params[:types].product([{"something" => "something"}]).to_h
If you want a different hash for each element of params[:types]:
hashes = [{ "something1"=>"something1" }, { "something2"=>"something2" },
{ "something3"=>"something3" }]
then
Hash[params[:types].zip(hashes)]
#=> {"type1"=>{"something1"=>"something1"},
# "type2"=>{"something2"=>"something2"},
# "type3"=>{"something3"=>"something3"}}

evading the use of if statements: shortening the code

I watched the video at https://gorails.com/blog/refactoring-if-statements but was looking for a more concise way of evading the use of multiple if or case statements.
The following works
def process(input)
commands = {
:q => Proc.new { puts "Goodbye" },
:tweet => Proc.new { puts "tweeting" },
:dm => Proc.new { puts "direct messaging"},
:help => Proc.new { puts "helping"}
}
commands[input.to_sym].call
end
process "tweet"
But how could i further shorten this ? I tried the following
def process(input)
commands = {
:q => { puts "Goodbye" },
:tweet => { puts "tweeting" },
:dm => { puts "direct messaging"},
:help => { puts "helping"}
}
commands[input.to_sym].to_proc.call
end
process "tweet"
but then i get the error
# syntax error, unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
# :q => { puts "Goodbye" },
# ^
Any suggestions please ?
Use the lambda syntax
def process(input)
commands = {
:q => ->{ puts "Goodbye" },
:tweet => ->{ puts "tweeting" },
:dm => ->{ puts "direct messaging"},
:help => ->{ puts "helping"}
}
commands[input.to_sym].to_proc.call
end
process "tweet"
Using the new Hash syntax can shorten this further:
def process(input)
{
q: ->{ puts "Goodbye" },
tweet: ->{ puts "tweeting" },
dm: ->{ puts "direct messaging"},
help: ->{ puts "helping"}
}[input.to_sym].call
end
process "tweet"
Use Kernel#proc:
Equivalent to Proc.new
def process(input)
commands = {
:q => proc { puts "Goodbye" },
:tweet => proc { puts "tweeting" },
:dm => proc { puts "direct messaging"},
:help => proc { puts "helping"}
}[input.to_sym].call
end
I am not sure whether what you suggest or I will suggest here improves the elegance or the readability of the code under question but you could shorten it even further by using the hash accessor pattern as in:
def process(input)
commands = {
:q => Proc.new { puts "Goodbye" },
:tweet => Proc.new { puts "tweeting" },
:dm => Proc.new { puts "direct messaging"},
:help => Proc.new { puts "helping"}
}[input.to_sym].call
end

Resources