It is possible to somehow use forward_missing_to to fetch/set hash values? - crystal-lang

I am building client for Asterisk Manager (AMI), which sending back "events" (strings in key-value format). Depending on event type the keys are different. Some known would be defined similar as a property, but I also want to access everything else similar way (I need only getter).
Below is example and I am confused about forward_missing_to, to fetch hash value (i.e. e.foo => e.data["foo"] and e.foo? => e.data["foo"]?
class Event
#data = Hash(String,String).new
def initialize(data : Hash(String, String))
#data.merge! data
end
# known field (event), here I could apply various validations etc.
def event=(#name : String)
#data["name"] = #name
end
def event : String
#data["name"].not_nil!
end
# Confusing about this:
forward_missing_to #data
end
Example of usage:
e = Event.new({"event" => "Test", "foo" => "bar"})
p e.event # => "Test" (only this works)
p e.foo # => expecting to get "bar"
p e.unknown # => should throw Unhandled exception: Missing hash key: "unknown" (KeyError)
p e.unknown? # => nil

forward_missing_to just means this:
e = Event.new({"event" => "Test", "foo" => "bar"})
# Same as invoking `foo` on the #data instance variable
# because you said forward_missing_to #data
e.foo
It doesn't mean invoking ["foo"] on #data.
You can do it like this:
macro method_missing(call)
{% if call.args.empty? && !call.named_args %}
#data[{{call.name.stringify}}]
{% else %}
{% raise "undefined method '#{call.name}' for #{#type}" %}
{% end %}
end
However, I wouldn't advise using method_missing, mainly because it might be eventually removed from the language (together with forward_missing_to).

Related

Type restriction for class members in case/when

Let's say I have following class Message representing a message in a chat.
class Message
JSON.mapping({
message_id: {type: Int32},
text: {type: String, nilable: true},
photo: {type: Photo, nilable: true},
sticker: {type: Sticker, nilable: true},
})
MESSAGE_TYPES = %w( text photo sticker )
{% for type in MESSAGE_TYPES %}
def {{ type.id }}?
! {{ type.id }}.nil?
end
{% end %}
end
Every message can be either text, or photo, or sticker. But only one of those. For instance, if message is a text, then photo and sticker properties are nil, but text is not nil. If message is a sticker, only sticker is not nil. And so on. Real class has much more direct types and inferred types.
Upon receiving this message, I want to process it in a way specific for particular type. Something like:
case message
when .text?
p message.text
when .photo?
p message.photo.file_id
when .sticker?
p message.sticker.file_id
end
Of course, it won't work, because in each when clause corresponding properties are nilable and undefined method 'file_id' for Nil.
Is there any way to restrict variable type in each case making sure that is message.sticker? then message.sticker is not nil and file_id exists?
Maybe I'm completely wrong in very approaching the problem and there better and cleaner ways to code this?
require "json"
class Photo
JSON.mapping(url: String)
end
struct Message
JSON.mapping(id: Int32, message: String|Photo)
end
p Message.from_json(%({"id": 5, "message": {"url": "http://example.org/"}}))
You could put all payload objects in one instance variable with a union type as Oleh Prypin suggested.
Another option, if you want to keep the current data layout, you could have the methods return the respective Type or nil and assign its value to a variable:
if text = message.text?
p text
if photo = message.photo?
p photo.file_id
if sticker = message.sticker?
p sticker.file_id
end

Unit testing assert that json response contains a list

When writing a unit test in phoenix framework how do you check if a json response contains a list.
Existing test is below which fails because children gets populated. I just want the test to tell me that my json response contains children and children is a list.
test "shows chosen resource", %{conn: conn} do
parent = Repo.insert! %Parent{}
conn = get conn, parent_path(conn, :show, parent)
assert json_response(conn, 200)["data"] == %{"id" => parent.id,
"children" => []}
end
I would use three asserts for this, using a pattern match assert first to assert the basic structure and extract id and children:
assert %{"id" => id, "children" => children} = json_response(conn, 200)["data"]
assert id == parent.id
assert is_list(children)
Note that this test will pass even if the map contains keys other than id and children.
With [json schema][2] you can generate a json to use with (https://github.com/jonasschmidt/ex_json_schema) to validate a full json structure.
iex> schema = %{
"type" => "object",
"properties" => %{
"foo" => %{
"type" => "string"
}
}
} |> ExJsonSchema.Schema.resolve
and
iex> ExJsonSchema.Validator.valid?(schema, %{"foo" => "bar"})
and remember have only one logical assertion per test” (http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/)

how to pass 2 data bag variables to template chef

I am trying to pass 2 data bags as variables into a template but it end in error message. Do anyone know how do i pass 2 databags to a template?
Recipe
db = data_bag_item('dbconnect', 'connection')
dbkey = data_bag_item('database', 'databasename')
template '/etc/config.cnf' do
source 'config.cnf.erb'
action :create
variables (
:dbcon => db,
:dbk => dbkey
)
end
Template
connection = mysql://<%= #dbcon['dbuser'] %>:<%= #dbcon['dbpasswd'] %>#<%= #dbcon['dbname'] %>/<%= #dbk['dbname'] %>
Okay. I got the answer.
I missed {} brackets in variables.
db = data_bag_item('dbconnect', 'connection')
dbkey = data_bag_item('database', 'databasename')
template '/etc/config.cnf' do
source 'config.cnf.erb'
action :create
variables ({
:dbcon => db,
:dbk => dbkey
})
end

Accessing properties in templates by their name

given the following pieces of code:
groovy:
binding = [key1: "val1"]
def f = new File('test.template')
engine = new GStringTemplateEngine()
template = engine.createTemplate(f).make(binding)
println template.toString()
test.template:
<% keyName = "key1" %>
Is there a way to access val1 by keyName in test.template?
This:
${ binding[keyName] }
does not work (No such property: key1 for class: groovy.lang.Binding). Any ideas? Maybe the name of the map holding the properties is different?
I know I could just write:
${ key1 }
but I need to access property key1 using variable keyName.
Not sure if this is better but I got the following to work (somewhat)
Map binding = [ keyName: 'key1', key1: "val1", m: [key1:'val100', key2:'val2']]
def f = new File('test.template')
def engine = new groovy.text.GStringTemplateEngine()
def template = engine.createTemplate(f).make(binding)
println template.toString()
with the following template:
$keyName
$key1
<%= m[keyName] %>
But this relies on a submap that holds the values you are looking for.
I can see scenarios where in the binding, you pass a list of fields you want to process or display (rather than knowing them ahead of time), so you would have to get the field names from a well-known variable and then process the others possibly thru a submap.

CustomFieldFormat Redmine

I created my own CustomFieldFormat :
init.rb
Redmine::CustomFieldFormat.map do |fields|
fields.register
MyCustomFieldFormat.new('my', :label => :label_test, :order => 1 + 0.6)
end
lib/my_custom_field_format.rb
class MyCustomFieldFormat < Redmine::CustomFieldFormat
def format_value(value, field_format)
"test"
end
def show_value(custom_value)
"test"
end
end
I would like to modify the value of the field while updating. (For example, return "test" and not the value stored in the database). But nothing happens, there is always the true value in the field and not "test". Why ?
Thanks