Parsing JSON in Ruby (like XPATH) - ruby

I have a JSON document returned from a query to the Google Books API, e.g:
{
"items": [
{
"volumeInfo": {
"industryIdentifiers": [
{
"type": "OTHER",
"identifier": "OCLC:841804665"
}
]
}
},
{
"volumeInfo": {
"industryIdentifiers": [
{
"type": "ISBN_10",
"identifier": "156898118X"...
I need the ISBN number (type: ISBN_10 or ISBN_13) and I've written a simple loop that traverses the parsed JSON (parsed = json.parse(my_uri_response)). In this loop, I have a next if k['type'] = "OTHER" which sets "type" to "OTHER".
How do I best extract just one ISBN number from my JSON example? Not all of them, just one.
Something like XPath search would be helpful.

JSONPath may be just what you're looking for:
require 'jsonpath'
json = #your raw JSON above
path = JsonPath.new('$..industryIdentifiers[?(#.type == "ISBN_10")].identifier')
puts path.on(json)
Result:
156898118X
See this page for how XPath translates to JSONPath. It helped me determine the JSONPath above.

how about:
parsed['items'].map { |book|
book['volume_info']['industryIdentifiers'].find{ |prop|
['ISBN_10', 'ISBN_13'].include? prop['type']
}['identifier']
}
If you receive undefined method [] for nil:NilClass this means that you have an element within items array, which has no volume_info key, or that you have a volume with a set of industryIdentifiers without ISBN. Code below should cover all those cases (+ the case when you have volumeInfo without industry_identifiers:
parsed['items'].map { |book|
identifiers = book['volume_info'] && book['volume_info']['industryIdentifiers']
isbn_identifier = idetifiers && identifiers.find{ |prop|
['ISBN_10', 'ISBN_13'].include? prop['type']}['identifier']
}
isbn_identifier && isbn_identifier['identifier']
}.compact
If you happen to have the andand gem, this might be written as:
parsed['items'].map { |book|
book['volume_info'].andand['industryIdentifiers'].andand.find{ |prop|
['ISBN_10', 'ISBN_13'].include? prop['type']
}.andand['identifier']
}.compact
Note that this will return only one ISBN for each volume. If you have volumes with both ISBN_10 and ISBN_13 and you want to get both, instead of find you'll need to use select method and .map{|i| i[:identifier]} in place of .andand['identifier'].

Related

Jmeter extracting values from response and sending to other requests

I have a JSON response below:
"books": [
{
"name" : "test1",
"id" : "T01"
},
{
"name" : "test2",
"id" : "T02"
},
{
"name" : "test3",
"id" : "T03"
},
]
I am extracting all respective ids and sending it as a body to another request.
Other request takes it as an array of strings but when I am extracting it, it is showing as integers:
Currently it shows: ids_ALL = [T01, T02, T03]
and I have to pass it like: ids_ALL = ["T01", "T02", "T03"]
Note: I am suffixing _ALL to get all ids.
Since it is not passing the array as string, I am getting an error.
Is there away to extract it and put it in array of strings or way to use post-processer and then convert the array and send to other request.
This one-liner will extract all the IDs and generate the JSON Array you're looking for:
vars.put('payload', (new groovy.json.JsonBuilder(new groovy.json.JsonSlurper().parse(prev.getResponseData()).books.id.collect()).toPrettyString()))
no other extractors are needed, you can refer the generated array as ${payload} later on where required
In Taurus it can be put into the JSR223 Block
More information:
Apache Groovy - Parsing and producing JSON
Apache Groovy - Why and How You Should Use It
You can use JSON Extractor or JSON JMESPath Extractor to extract all the ids from the response.
Place a JSR223 Post processor just below the JSON Path Extractor to create a list of Strings (ids)
def idCount=vars.get("booksIds_matchNr").toInteger()
def lstIds=[]
for(i in 1..idCount){
String currentId=vars.get("booksIds_" + i)
lstIds.add("\""+ currentId + "\"" )
//lstIds.add("${currentId}" )
}
vars.putObject("lstIds",lstIds)
You can access the list with vars.getObject("lstIds")
List of strings could be seen in the view result tree.
Another quick solution
Add a JSR223 Post Processor below the JSON Extractor to create strings with the available values.
String booksIds_ALL=vars.get("booksIds_ALL")
def lstIds = "\"" + booksIds_ALL.replace(",", "\",\"") + "\""
vars.putObject("lstIds",lstIds)

How to target something in an object like Xpath does?

I use Xpath to get some elements of an XML. Is there a similar way to obtain it for an object/hash?
I need get the value of a JSON object using some kind of selectors, which would need to be flexible enough because this JSON won't always be structured the same way.
Something like this is xpath would have been //data/children/*/title for instance.
Is there something similar for objects? I don't want to convert my object to an XML, it would bring other problems.
Ruby 2.7 introduced pattern matching, which might solve your problem. For example:
data = {
data: {
children: [
{
title: 'Find me'
},
{
title: 'I am wrong'
}
]
}
}
case data
in {data: {children: [{title: 'Find me'}, *rest]}}
puts 'found'
else
puts 'not found'
end
In this case, Ruby checks the data structure and prints 'ok' if {title: 'Find me'} is in children key.

Get parent node based on some condition in ruby

i have a hash like below.
prop = {"Pets"=>[]},
{"Misc"=>["HOA Frequency: (C101)"], "photos"=>nil},
{"Legal and finance"=>["HOA fee: $300.0"], "photos"=>nil}
I need to get Legal and finance nodes based on some condition.
I tried like below.
prop.find { |feature| feature.keys.include?("Legal and finance") }
But sometimes HOA fee will be under different node. It might be in "Finance" or "Legal and Finance" or "Home Finance" like
{"Finance"=>["HOA fee: $300.0"], "photos"=>nil} or
{"Home Finance"=>["HOA fee: $300.0"], "photos"=>nil}
So i need to get that complete node by checking whether any node contains text as "HOA Fee" as value.
prop.find do |feature|
feature.values.flatten.compact.any? do |value|
value.include?("HOA Fee")
end
end
This is a very messy data structure, however.
I would strongly advise you to refactor the code to store data in well-defined objects, not hashes of hashes of arrays...
I would do something like this:
prop.find { |hash| hash.keys.any? { |key| key.downcase.include?('finance') } }
#=> { "Legal and finance" => ["HOA fee: $300.0"], "photos" => nil }

Obtain index of random element from json array in JMeter Json Path Post Processor

I have strange case for jmeter. Imagine that we have an json array with elements like this:
{
"id" : 123456,
"name": "TEST"
}
So I want to get random element from array that has id. For this case I use Json Path PostProcessor with expression like this $.elements[?(#.id)]
But for some reasons I need an index of this element. So I can create BeanShellPostProcessor generate random index and then use same Json Path PostProcessor with expression like this $.elements[${PARAM_ElementIndex}].
But in some cases this array can be empty and Json Path PostProcessor wil fail with exception like this:
jmeter.extractor.json.jsonpath.JSONPostProcessor: Error processing JSON content in PARAM_ResumeId, message:No results for path: $['elements'][0]['id']
So may be someone can suggest any solution
I would recommend use Groovy instead of Beanshell as:
Well-behaved Groovy scripts can be compiled into bytecode therefore performance will be much higher
Groovy has built-in JSON support
So given you have JSON Response like:
{
"elements": [
{
"id": 123456,
"name": "TEST"
},
{
"id": 7890,
"name": "TEST2"
}
]
}
You can extract random ID along with its index using the following example Groovy code in the JSR223 PostProcessor:
import groovy.json.JsonSlurper
import java.util.concurrent.ThreadLocalRandom
String response = prev.getResponseDataAsString()
def jsonSlurper = new JsonSlurper()
def json = jsonSlurper.parseText(response)
int size = json.elements.size
if (size > 0){
def randomIndex = ThreadLocalRandom.current().nextInt(size)
def value = json.elements.get(randomIndex).id
log.info('Index: ' + randomIndex)
log.info('Value: ' + value)
}
Demo:
References:
Parsing and producing JSON
Beanshell vs JSR223 vs Java JMeter Scripting: The Performance-Off You've Been Waiting For!

Iterate an array of hashes

I have a hash with a key of cities and the value is an array of hashes containing location data. It looks like this:
#locations = {
"cities"=>[
{"longitude"=>-77.2497049, "latitude"=>38.6581722, "country"=>"United States", "city"=>"Woodbridge, VA"},
{"longitude"=>-122.697236, "latitude"=>58.8050174, "country"=>"Canada", "city"=>"Fort Nelson, BC"},
...
]
}
I'd like to iterate through and print all the values for the key city:
Woodbridge, VA
Fort Nelson, BC
...
I can't say why would you have that structure, anyway, in the data format you have above, you would access it like
#locations[1].each { |c| p c["city"] }
Although, this implies that you should always expect second object in the array to be the required cities array. Further you need to put in required nil check.
For your corrected data format:
#locations = { "cities"=>[
{ "longitude"=>-77.2497049,
"latitude"=>38.6581722,
"country"=>"United States",
"city"=>"Woodbridge, VA"},
{ "longitude"=>-122.697236,
"latitude"=>58.8050174,
"country"=>"Canada",
"city"=>"Fort Nelson, BC" }] }
#locations["cities"].each { |h| puts h["city"] }
Woodbridge, VA
Fort Nelson, BC
or to save in an array:
#locations["cities"].each_with_object([]) { |h,a| a << h["city"] }
#=> ["Woodbridge, VA", "Fort Nelson, BC"]
As suggested by others, you have to do the exact same thing but let me explain whats happening in there.
Your example is an array and has multiple elements which could be just string like cities or an array of hashes like you mentioned.
So in order to iterate through the hashes and get the city values printed, you first of all have to access the array that has hashes. By doing so
#locations["cities"]
=> [{"longitude"=>-77.2497049, "latitude"=>38.6581722, "country"=>"United States", "city"=>"Woodbridge, VA"}, {"longitude"=>-122.697236, "latitude"=>58.8050174, "country"=>"Canada", "city"=>"Fort Nelson, BC"}]
Now that you have go the array you required, you can just integrate through them and get the result printed like this
#locations["cities"].map{|hash| p hash['city']}
In case your getting nil errors as you have stated in comments, just see what happens when you try to access the array of hashes. if you still are experiencing issues, then you may have to provide the full input so as to understand where the problem is.

Resources