Parse complex YAML-structure with YAML.mapping - parsing

I have complex structure in YAML like this
yaml = <<-STR
'Tunisie Telecom':
regex: 'StarTrail TT[);/ ]'
device: 'smartphone'
model: 'StarTrail'
Palm:
regex: '(?:Pre|Pixi)/(\d+)\.(\d+)|Palm|Treo|Xiino'
device: 'smartphone'
models:
- regex: '((?:Pre|Pixi))/(\d+\.\d+)'
model: '$1 $2'
- regex: 'Palm(?:[ \-])?((?!OS|Source|scape)[a-z0-9]+)'
model: '$1'
STR
I'm try to parse it with YAML.mapping:
class Mobile
YAML.mapping(
name: Hash(String, Hash(String, String))
)
end
puts Mobile.from_yaml(yaml)
And got parse exception:
Missing yaml attribute: name at line 1, column 1 (YAML::ParseException)
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/yaml/nodes/nodes.cr:30:9 in 'raise'
from mobiles_mapping.cr:0:3 in 'initialize'
from mobiles_mapping.cr:20:3 in 'new'
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/yaml/from_yaml.cr:2:3 in 'from_yaml'
from mobiles_mapping.cr:25:1 in '__crystal_main'
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/crystal/main.cr:11:3 in '_crystal_main'
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/crystal/main.cr:112:5 in 'main_user_code'
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/crystal/main.cr:101:7 in 'main'
from /usr/local/Cellar/crystal-lang/0.24.2_1/src/crystal/main.cr:135:3 in 'main'
But if I try to parse with YAML.parse:
{"Tunisie Telecom" => {"regex" => "StarTrail TT[);/ ]", "device" => "smartphone", "model" => "StarTrail"}, "Palm" => {"regex" => "(?:Pre|Pixi)/(d+).(d+)|Palm|Treo|Xiino", "device" => "smartphone", "models" => [{"regex" => "((?:Pre|Pixi))/(d+.d+)", "model" => "$1 $2"}, {"regex" => "Palm(?:[ -])?((?!OS|Source|scape)[a-z0-9]+)", "model" => "$1"}]}}
What's my mistake?
Online example: https://play.crystal-lang.org/#/r/41yk
UPD: I understood: macros can't found key 'name' but how can parse without key => val like in my case where we have only key?

What you have is a Hash(String, Mobile), where Mobile is similar to
class Mobile
YAML.mapping({
regex: String,
device: String,
model: String?,
models: Array(...)?
})
end
So, to parse that you should call Hash(String, Mobile).from_yaml.
The code you wrote would work (ish) if your YAML was:
name:
'Tunisie Telecom':
regex: 'StarTrail TT[);/ ]'
device: 'smartphone'
model: 'StarTrail'
Palm:
regex: '(?:Pre|Pixi)/(\d+)\.(\d+)|Palm|Treo|Xiino'
device: 'smartphone'
models:
- regex: '((?:Pre|Pixi))/(\d+\.\d+)'
model: '$1 $2'
- regex: 'Palm(?:[ \-])?((?!OS|Source|scape)[a-z0-9]+)'
model: '$1'

Related

Convert YAML string to JSON using Ruby

I am able to convert a YAML file to JSON, but I am not able to convert a YAML string to JSON. Is there any other way to convert YAML string to JSON?
Sample Input
---
:name:
:firstname: Guru
:lastname: Shyam
Expected output
{
"name": {
"firstname": "Guru",
"lastname": "Shyam"
}
}
Try Pysch.load
data = "---\n:name:\n :firstname: Guru\n :lastname: Shyam\n"
Psych.load(data)
-> {
:name => {
:firstname => "Guru",
:lastname=> "Shyam"
}
}
YAML.load_file might help you.
Btw, it is an alias for Psych but has a more convenient name, and included in ruby standart library.
[2] pry(main)> .cat data.yml
---
:name:
:firstname: Guru
:lastname: Shyam
[3] pry(main)> require 'yaml'
=> true
[4] pry(main)> puts YAML.load_file('data.yml').to_json
{"name":{"firstname":"Guru","lastname":"Shyam"}}
=> nil

combine keys in array of hashes

I map results of my query to create an array of hashes grouped by organisation_id like so:
results.map do |i|
{
i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}
}
}
a capability is defined outside the map.
The result looks like:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4}}}, {1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{12=>5}}}, {2 => {...}}...]
For every organisation_id there is a separate entry in the array. I would like to merge these hashes and combine the scores key as so:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4, 12=>5}}}, {2=>{...}}... ]
EDIT
To create the results I use the following AR:
Valuation.joins(:membership)
.where(capability: capability)
.select("valuations.id, valuations.score, valuations.capability_id, valuations.membership_id, memberships.location_id, memberships.organisation_id")
.map(&:serializable_hash)
A Valuation model:
class Valuation < ApplicationRecord
belongs_to :membership
belongs_to :capability
end
A Membership model:
class Membership < ApplicationRecord
belongs_to :organisation
belongs_to :location
has_many :valuations
end
results snippet:
[{"id"=>1, "score"=>4, "capability_id"=>1, "membership_id"=>1, "location_id"=>26, "organisation_id"=>1}, {"id"=>16, "score"=>3, "capability_id"=>1, "membership_id"=>2, "location_id"=>36, "organisation_id"=>1}, {"id"=>31, "score"=>3, "capability_id"=>1, "membership_id"=>3, "location_id"=>26, "organisation_id"=>2}, {"id"=>46, "score"=>6, "capability_id"=>1, "membership_id"=>4, "location_id"=>16, "organisation_id"=>2}...
I'll assume for each organization: the name, taglist and organization_id remains the same.
your_hash = results.reduce({}) do |h, i|
org_id = i['organisation_id']
h[org_id] ||= {
name: capability.name,
tags: capability.taglist,
organisation_id: org_id,
scores: {}
}
h[org_id][:scores][i['location_id']] = i['score']
# If the location scores are not strictly exclusive, you can also just +=
h
end
I believe this works, but data is needed to test it.
results.each_with_object({}) do |i,h|
h.update(i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}) { |_,o,n|
o[:scores].update(n[:score]); o }
}
end.values
This uses the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged. Please consult the doc for the contents of each of the block variables _, o and n.
Assume, that result is your final array of hashes:
result.each_with_object({}) do |e, obj|
k, v = e.flatten
if obj[k]
obj[k][:scores] = obj[k][:scores].merge(v[:scores])
else
obj[k] = v
end
end

Nested dynamic array rails

I have created an array
steps = [{'title' =>'abc','content' =>'click this', 'target' => 'bca'}]
tours = ['id'=>'tour', 'steps:' => "#{steps}"]
puts tours
Getting following output :
{"id"=>"tour", "steps:"=>"[{\"title\"=>\"abc\", \"content\"=>\"click this\", \"target\"=>\"bca\"}]"}
The structure of the output is right but i don't want these \ in the output.
What should i do to remove these \.
Thanks!
In ruby "#{}" invoke the to_s method on the object. You can check it run the following code: steps.to_s.
Just use:
tours = ['id'=>'tour', 'steps:' => steps]
Because this:
"[{\"title\"=>\"abc\", \"content\"=>\"click this\", \"target\"=>\"bca\"}]"
is a string representation of:
[{'title' =>'abc','content' =>'click this', 'target' => 'bca'}]
Зелёный has the direct answer for you, however, there's a more pressing issue I would point out -- I think you're getting confused between {hashes} and [arrays]
--
An array is a set of unordered data:
array = [3, 4, 5, 6, 0, 5, 3, "cat", "dog"]
Arrays are mainly used for non-sequential collections of data, a good example being product_ids in a shopping cart.
Arrays can only be identified by using the location of the data inside the array:
array[1] # -> 4
array[2] # -> 5
--
A hash is a collection of key:value pairs:
hash = {name: "Greg", type: "cat"}
Hashes are used when you wish to assign multiple values to a single piece of data, and can be called by referencing the "key" of the hash:
hash["name"] #-> Greg
hash["type"] #-> cat
Whilst you can create an array of hashes:
hash_array = [{name: "Greg", type: "cat"}, {name: "Sulla", type: "Dog"}]
... the problem with this is that you cannot call the hashes directly - they have to be through the array:
hash_array["name"] # -> error
hash_array[0]["name"] #-> "Greg"
Thus, I'd use the following in your example:
steps = {'title' =>'abc','content' =>'click this', 'target' => 'bca'}
tours = {id: 'tour', steps: steps}
tours.inspect #-> { id: "tour", steps: { "title" => "abc", "content" => "click this", "target" => "bca" }

Redis: Expand key references in values to their values

I am fairly new to redis. I have a CMS that outputs JSON to redis in a structure like this:
partial:1 => {id: 1, title: 'Foo1', ...}
partial:2 => {id: 2, title: 'Foo2', ...}
partial:3 => {id: 3, title: 'Foo3', ...}
page:home => {
id: 'Homepage',
teaser1: 'partial:1',
teaser2: 'partial:2',
teaser3: {type: Slider, content: 'partial:3'}
}
So the JSON can contain other redis-keys that conform to a certain naming scheme in its structure. What I would like to have is a way to query redis so that when I get the page:home-key the references to other keys in the json get 'expanded' to their respective values, like this:
{
id: 'Homepage',
teaser1: {id: 1, title: 'Foo1', ...},
teaser2: {id: 2, title: 'Foo2', ...},
teaser3: {type: Slider, content: {id: 3, title: 'Foo3', ...}
}
Is this possible and how could it be achieved?
Yes. it is quite possible. Here's a working approach:
Create a function to convert your redis hashes (e.g: partial:*,page:home) to lua tables: hgetall
Create a function to check if the given redis hashname conforms with your naming scheme, if so use hgetall to convert it to a lua table; otherwise return the same value: evaluate_hash
Create a function to evaluate and convert all properties of a given hash name and return it as a json: expand
Here's a simple implementation:
--Script: redis_expand_as_json.lua
--Function to Convert a given hash name (e.g: partial:1..n, page:home) to a lua table
local function hgetall(a)local b=redis.call('HGETALL',a)local c={}local d;for e,f in ipairs(b)do if e%2==1 then d=f else c[d]=f end end;return c end
--Function to check if the given value conforms with a naming scheme,
-- if so convert it to a lua table; otherwise return the values as is.
local function evaluate_hash(value)
local pattern = "partial:%d+"
if string.match(value, pattern) then
return hgetall(value)
else
return value
end
end
--Function to convert a given hash_name to a lua table,
-- iterate all elements and convert them to lua table if necessary
-- returns the table as a json object
local function expand(hash_name)
local obj_table = hgetall(hash_name)
for k, val in pairs(obj_table) do
obj_table[k] = evaluate_hash(val)
end
return cjson.encode(obj_table)
end
local page = KEYS[1]
local json_result = expand(page) or {}
redis.log(redis.LOG_NOTICE, tostring(json_result))
return json_result
Testing in console:
redis-cli -c hmset "partial:1" id 1 title foo1
OK
redis-cli -c hmset "partial:2" id 2 title foo2
OK
redis-cli -c hmset 'page:home' id 'Homepage' teaser1 'partial:1' teaser2 'partial:2'
OK
redis-cli EVAL "$(cat redis_expand_as_json.lua)" 1 'page:home'
{"teaser1":{"id":"1","title":"foo1"},"teaser2":{"id":"2","title":"foo2"},"id":"Homepage"}
I don't know Redis but perhaps this works for you:
local T=[[
partial:1 => {id: 1, title: 'Foo1', ...}
partial:2 => {id: 2, title: 'Foo2', ...}
partial:3 => {id: 3, title: 'Foo3', ...}
page:home => {
id: 'Homepage',
teaser1: 'partial:1',
teaser2: 'partial:2',
teaser3: {type: Slider, content: 'partial:3'}
}
]]
local D={}
for k,v in T:gmatch("(%w+:%w+)%s*=>%s*(%b{})") do
D[k]=v
end
print((T:gsub("'(.-)'",D)))

cannot understand rails activerecord typecast reasons

consider that i have a migration as follows
create_table :dummies do |t|
t.decimal :the_dummy_number
end
i instantiate like the following
dummy = Dummy.new
dummy.the_dummy_number = "a string"
puts dummy.the_dummy_number
the output for the above is
0.0
how did this happen? since i assign a wrong value shouldn't it raise an error?
The biggest problem is the following.
Since it automatically converts my validate method fails miserably.
update-the validate method
validate :is_dummy_number_valid, :the_dummy_number
def is_dummy_number_valid
read_attribute(:the_dummy_number).strip()
end
The reason that this does not work as you expect is that the underlying ruby implementation of BigDecimal does not error when passed a string.
Consider the following code
[ 'This is a string', '2is a string', '2.3 is also a string',
' -3.3 is also a string'].each { |d| puts "#{d} = #{BigDecimal.new(d)}" }
This is a string = 0.0
2is a string = 2.0
2.3 is also a string = 2.3
-3.3 is also a string = -3.3
So BigDecimal scans the string and assigns anything at the beginning of the string that could be a decimal to its value.
If you set your model up like this
class Dummy < ActiveRecord::Base
validates_numericality_of :the_dummy_number
end
Then the validation should work fine
>> d=Dummy.new(:the_dummy_number => 'This is a string')
=> #<Dummy id: nil, the_dummy_number: #<BigDecimal:5b9230,'0.0',4(4)>, created_at: nil, updated_at: nil>
>> puts d.the_dummy_number
0.0
=> nil
>> d.valid?
=> false
>> d.errors
=> #<ActiveRecord::Errors:0x5af6b8 #errors=#<OrderedHash
{"the_dummy_number"=>[#<ActiveRecord::Error:0x5ae114
#message=:not_a_number, #options={:value=>"This is a string"}
This works because the validates_numericality_of macro uses the raw_value method to get at the value before it was typecast and assigned to the internal decimal value.

Resources