Ruby Nested Hash Merge - ruby

Given something like this:
hey = {
some_key: {
type: :object,
properties: {
id: { type: :string, example: '123', description: 'Id' },
created_at: { type: :string, example: '2019-02-14 14:13:55'},
updated_at: { type: :string, example: '2019-02-14 14:13:55'},
type: { type: :string, example: 'something', description: 'Resource type' },
token: { type: :string, example: 'token', description: 'Some description of token' }
}
}
}
I would like to go through all keys until I find one named properties, then mutate its content such that the keys become the value of a description key if it doesn't exit in its nested hash.
So for the example above, the hash would end up like this:
hey = {
some_key: {
type: :object,
properties: {
id: { type: :string, example: '123', description: 'Id' },
created_at: { type: :string, example: '2019-02-14 14:13:55', description: 'Created At'},
updated_at: { type: :string, example: '2019-02-14 14:13:55', description: 'Updated At'},
type: { type: :string, example: 'something', description: 'Resource type' },
token: { type: :string, example: 'token', description: 'Some description of token' }
}
}
}
created_at and updated_at didn't have a description.
It should also handle if token, for instance, had a properties property.
I came up with a solution that works but I am really curious on how I can improve it?
My solution below:
def add_descriptions(hash)
return unless hash.is_a?(Hash)
hash.each_pair do |key, value|
if key == :properties
value.each do |attr, props|
if props[:description].nil?
props.merge!(description: attr.to_s)
end
end
end
add_descriptions(value)
end
end

As I understand all you know about the hash hey is that it is comprised of nested hashes.
def recurse(h)
if h.key?(:properties)
h[:properties].each do |k,g|
g[:description] = k.to_s.split('_').map(&:capitalize).join(' ') unless
g.key?(:description)
end
else
h.find { |k,obj| recurse(obj) if obj.is_a?(Hash) }
end
end
recurse hey
#=> {:id=>{:type=>:string, :example=>"123", :description=>"Id"},
# :created_at=>{:type=>:string, :example=>"2019-02-14 14:13:55",
# :description=>"Created At"},
# :updated_at=>{:type=>:string, :example=>"2019-02-14 14:13:55",
# :description=>"Updated At"},
# :type=>{:type=>:string, :example=>"something",
# :description=>"Resource type"},
# :token=>{:type=>:string, :example=>"token",
# :description=>"Some description of token"}}
The return value is the updated value of hey.

Related

How to add element on graphql return fields

Im a newbie in Ruby and GraphQL
Currently i have such Mutations module
module Mutations
class ProductCreate < BaseMutation
# TODO: define return fields
# field :post, Types::PostType, null: false
type Types::ProductType
# TODO: define arguments
argument :title, String, required: true
argument :vendor, String, required: false
argument :store, ID, required: true
# TODO: define resolve method
def resolve(title:, vendor:, store:)
Product.create!(title: title, vendor: vendor, store: store)
end
end
end
and when i call
mutation {
productCreate(input: {store:"61d6f33a58c4dc4e8a1a0536", title: "Sweet new product", vendor: "JadedPixel"})
{
_id
}
}
Result is
{
"data": {
"productCreate": {
"_id": "61de591c58c4dcb08dffafa9"
}
}
}
I would like to add additional paramenter to query and also get additional paramenter in result
So, my question is
What should i change in code
mutation {
productCreate(input: {title: "Sweet new product", productType: "Snowboard", vendor: "JadedPixel"}) {
product {
id
}
}
}
to get result like this
{
"productCreate": {
"product": {
"id": "1071559610"
}
}
}
I found solutions
Just need to change code like this
module Mutations
class ProductCreate < BaseMutation
field :product, Types::ProductType, null: true
# TODO: define arguments
argument :title, String, required: true
argument :vendor, String, required: false
argument :store, ID, required: true
# TODO: define resolve method
def resolve(title:, vendor:, store:)
record = Product.create!(title: title, vendor: vendor, store: store)
{ product: record }
end
end
end
source of an example
https://www.keypup.io/blog/graphql-the-rails-way-part-2-writing-standard-and-custom-mutations

Forest Admin: "Invalid Data URI: nil" when i try to build a Import data custom action

I'm beginner in RoR and I would like to implement a custom action in forest admin for import data (csv).
http://doc.forestadmin.com/developers-guide/#triggering-an-action-from-the-collection
I have my actions_controller (controllers/forest/actions_controller.rb):
class Forest::ActionsController < ForestLiana::ApplicationController
require 'data_uri'
def bulk_import
uri = URI::Data.new(params.dig('data', 'attributes', 'values', 'file'))
uri.data.force_encoding('utf-8')
CSV.parse(uri.data).each do |row|
Geo.create!({
:departement=> row[0],
:slug => row[1],
:nom => row[2],
:nom_simple => row[3],
})
end
render json: { success: 'Data successfuly imported!' }
end
end
My collection (lib/forest_liana/collections/geo.rb):
class Forest::Geo
include ForestLiana::Collection
collection :geos
action 'bulk_import', global: true,
fields: [{
field: 'importer csv', type: 'File', isRequired: true, description: "Personal description",
}]
end
And i add in my routes:
namespace :forest do
post '/actions/bulk_import' => 'actions#bulk_import'
end
This doesn't work... :/
The message error:
Started OPTIONS "/forest/actions/bulk_import" for 127.0.0.1 at 2017-08-19 18:52:30 +0200
Started POST "/forest/actions/bulk_import" for 127.0.0.1 at 2017-08-19 18:52:30 +0200
Processing by Forest::ActionsController#bulk_import as HTML
Parameters: {"data"=>{"attributes"=>{"ids"=>[], "values"=>{"importer csv"=>"data:text/csv;base64,77u/ZGVwYXJ0ZW1lbnQsIHNsdWcsIG5vbSwgbm9tX3NpbXBsZQ0iMSwiIjAxIiIsIiJvemFuIiIsIiJPWkFOIiIi"}, "collection_name"=>"geos"}, "type"=>"custom-action-requests"}}
Completed 500 Internal Server Error in 3ms (ActiveRecord: 0.0ms)
URI::InvalidURIError - Invalid Data URI: nil:
data_uri (0.1.0) lib/data_uri/uri.rb:15:in `initialize'
app/controllers/forest/actions_controller.rb:4:in `bulk_import'
In your configuration, the field is named importer csv so it is this field name that will be sent in the request.
In lib/forest_liana/collections/geo.rb
class Forest::Geo
include ForestLiana::Collection
collection :geos
action 'bulk_import', global: true,
fields: [{
field: 'importer csv', type: 'File', isRequired: true, description: "Personal description",
}]
end
In your controller, you should do:
uri = URI::Data.new(params.dig('data', 'attributes', 'values', 'importer csv'))
Instead of :
uri = URI::Data.new(params.dig('data', 'attributes', 'values', 'file'))
It looks like importer csv should be the name of the action and not the field.
If you want to name your action like this, here is the suitable configuration:
class Forest::Geo
include ForestLiana::Collection
collection :geos
action 'Importer CSV', global: true,
fields: [{
field: 'importer csv', type: 'File', isRequired: true, description: "Personal description",
}]
end
quick suggestion write require 'data_uri' above the class name.

How to populate the response model for http_code 400/401 in swagger ui using ruby?

How to populate the response model for http_code 400/401 in swagger ui using ruby? I want to add the response model for 401 error.My code looks something like this:
user_entity.rb:
module something
module V1
class UserEntity < Grape::Entity
expose :id, documentation: { type: "String", desc: "User id" }
expose :phone, documentation: { type: "String", desc: "Registered phone number" }
expose :email, documentation: { type: "String", desc: "Email" }
expose :created_at, documentation: { type: "String", desc: "format:yyyy-mm-ddThh:mm:ss.364+(gmt) for eg:\"2015-10-04T15:33:39.364+04:00\"" }
expose :updated_at, documentation: { type: "String", desc: "format:yyyy-mm-ddThh:mm:ss.364+(gmt) for eg:\"2015-10-04T15:33:39.364+04:00\"" }
expose :access_token, if: lambda { |object, options| options[:show_access_token] == true }
expose :access_token_expires, if: lambda { |object, options| options[:show_access_token] == true }
private
def id
object.id.to_s
end
end
end
end
user_resource.rb:
module something
module V1
class UsersResource < Grape::API
include something::V1::Defaults
resource :users, desc: 'Operations about users' do
desc "Returns all users", {
headers: {
"Authorization" => {description: "pass the access token as Bearer",
required: true
}
},
http_codes: [
[401, 'Not authorized: The access token does not exist or is invalid'],
[200, 'Success',UserEntity]
],
detail: 'This endpoint returns all the users that have successfully registered with a valid phone number.'
}
get do
User.all.to_a
end
.....
I figured it out myself. I just added all the error codes i needed in my defaults.rb and added it to my http_codes

writing a rspec test to add an item to a hash

I am getting a NoMethodError
for my code but I have defined the add method it says it is missing.
I am trying to add an item to a hash that already exists.
The hash is the dishes and I am trying to use the add method.
The test:
require 'menu'
describe Menu do
it 'has a menu' do
expect(subject.respond_to?(:dishes)).to be true
end
it 'displays dishes and prices' do
expect(subject.dishes).to eq [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
it 'can add dishes to it' do
menu = Menu.new
menu.add_dish("Icecream", 4.80)
expect(subject.dishes).to eq [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 },
{ name: 'icecream', price: 4.80 }
]
end
end
the methods
class Menu
def initialize
#dishes = []
end
def dishes
#dishes = [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
def add_dish(name, price)
#dishes << { name: name, price: price }
end
end
Thank you
The answer of Ryan-Neal Mes solves the NoMethodError, but there are many other problems in your code.
You repeat your self, and you should make your code dry (Don't Repeat Yourself principle)
while you want to add a hash to the list of dishes which is it self a list of hashes, you force the object that needs to call the add method to provide the parameters in a particular order, than the method constructs the hash, so every time you need to call it you need to return to it to see the order of parameters.
the dishes method is wrong, because each time you call it, it assigns the initial array to the #dishes variable. In this case the add_dishes method will have no effect, since the added dish will be deleted the next time you call the dishes method.
your examples are not expressive, so if a test did not pass, you cannot know from the printed messages what's the problem. OK, this is not a big deal in this small example, but in a big application, specs expressiveness is of a higher value.
here the test examples
require 'menu'
describe Menu do
# every time you call the function dishes in an example
# it will be declared and it will return this array
let :dishes do
[
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
# explicit definition of the subject
subject { Menu.new }
# a shorter yet more expressive version of
# expect(subject.respond_to?(:dishes)).to be true
it { is_expected.to respond_to(:dishes) }
# You should always group the examples that test
# the same method
describe '#dishes' do
# it 'displays dishes and prices' do
it 'returns the list of dishes' do
expect(subject.dishes).to eq dishes
end
end
describe "#add_dish" do
# it 'can add dishes to it' do
it "adds the given dish to the list of dishes" do
new_dish = {name: 'salad', price: 4.0 }
expect {
subject.add_dish(new_dish)
}.to change(subject.dishes, :count).by(1)
expect(subject.dishes).to include new_dish
end
end
end
so here is the class definition
class Menu
# you don't need to declare the method dishes
# since this is what attr_reader will do
attr_reader :dishes
def initialize
# this will set the #dishes only once
# but you code #dishes = [...] will return
# the same list every time you call it and
# all the dishes you add through the #add method
# will be deleted.
#dishes = [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
# dish is a hash {name: '', price: ''}
def add_dish(dish)
#dishes << dish
end
end
so now run rspec --format doc --color and see who expressive are the messages.
It's a bit difficult to get this working without your code, but the problem is pretty straight forward.
Try the edited code below. Note the changes to the spec initialize the menu and add method adds to the instance variable #dishes.
require 'menu'
describe Menu do
it 'displays dishes and prices' do
expect(Menu.new.dishes).to eq [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
it 'can add dishes to it' do
menu = Menu.new.add("Icecream", 4.80)
expect(menu.dishes).to eq [
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 },
{ name: 'icecream', price: 4.80 }
]
end
end
class Menu
def initialize
#dishes = []
end
def dishes
#dishes ||=
[
{ name: 'Burger', price: 10.95 },
{ name: 'Pizza', price: 14.00 },
{ name: 'Salad', price: 7.60 },
{ name: 'fries', price: 2.90 }
]
end
def add(name, price)
#dishes << { name: name, price: price }
end
end
Hope this helps
It looks like you've got a couple of problems with this code. First, because the add method is not declared as a class method (i.e. def self.add) you can't call it as a class method (as you've seen, Menu.add says NoMethodError). Instead, you'll need to create an instance of the Menu class in your test, perhaps using let:
describe Menu do
let(:menu) { Menu.new }
it 'can add dishes to it' do
menu.add("Icecream", 4.80)
# test your expectations...
end
end
Lastly, as the add method is currently defined, it doesn't modify #dishes but rather just returns a new hash, so your expectation will fail. You'll need to make the add method append the values, perhaps like this:
def add(name, , price)
#dishes << {name: name, price: price}
end
Ah I see your problem. You need to initialize your menu. Add is not a static method. So you need something like,
Menu.new.add(blah, blah)
Look at:
Menu.add("Icecream", 4.80)
This method is wrong. It needs to be:
Menu.new.add("Icecream", 4.80)
or you need something like:
menu = Menu.new
menu.add("Icecream", 4.80)

elastic search object association querying through params

I'm having some difficulty with Elastic Search and Tire not returning any results. I'm using Ruby 1.9.3 and Rails 3.2.11.
In my controller I'm calling:
#location_id = 1
#listings = Listing.search(params.merge!(location_id: #location_id))
In my listing model I have
mapping do
indexes :id, type: 'integer'
...
indexes :author do
indexes :location_id, :type => 'integer', :index => :not_analyzed
...
end
def self.search(params={})
tire.search(load: true, page: params[:page], per_page: 20) do |search|
search.query { string params[:query], :default_operator => "AND" } if params[:query].present?
search.filter :range, posted_at: {lte: DateTime.now}
search.filter :term, "author.location_id" => params[:location_id]
end
I have 300 results which all have the location_id of 1 in the database so I can't seem to figure out why it's returning a nil set? If I comment out the author.location_id search filter line it returns all other results as expected?
There are several things which needs to be adressed in a situation like yours. Let's start with a fully working code:
require 'active_record'
require 'tire'
require 'logger'
# Tire.configure { logger STDERR }
# ActiveRecord::Base.logger = Logger.new(STDERR)
Tire.index('articles').delete
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
ActiveRecord::Schema.define(version: 1) do
create_table :articles do |t|
t.string :title
t.integer :author_id
t.date :posted_at
t.timestamps
end
create_table :authors do |t|
t.string :name
t.integer :number, :location_id
t.timestamps
end
add_index(:articles, :author_id)
add_index(:authors, :location_id)
end
class Article < ActiveRecord::Base
belongs_to :author, touch: true
self.include_root_in_json = false
include Tire::Model::Search
include Tire::Model::Callbacks
mapping do
indexes :title
indexes :author do
indexes :location_id, type: 'integer'
end
end
def self.search(params={})
tire.search load: {include: 'author'} do |search|
search.query do |query|
query.filtered do |f|
f.query { params[:query].present? ? match([:title], params[:query], operator: 'and') : match_all }
f.filter :range, 'posted_at' => { lte: DateTime.now }
f.filter :term, 'author.location_id' => params[:location_id]
end
end
end
end
def to_indexed_json
to_json( only: ['title', 'posted_at'], include: { author: { only: [:location_id] } } )
end
end
class Author < ActiveRecord::Base
has_many :articles
after_touch do
articles.each { |a| a.tire.update_index }
end
end
# -----
Author.create id: 1, name: 'John', location_id: 1
Author.create id: 2, name: 'Mary', location_id: 1
Author.create id: 3, name: 'Abby', location_id: 2
Article.create title: 'Test A', author: Author.find(1), posted_at: 2.days.ago
Article.create title: 'Test B', author: Author.find(2), posted_at: 1.day.ago
Article.create title: 'Test C', author: Author.find(3), posted_at: 1.day.ago
Article.create title: 'Test D', author: Author.find(3), posted_at: 1.day.from_now
Article.index.refresh
# -----
articles = Article.search query: 'test', location_id: 1
puts "", "Documents with location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
articles = Article.search query: 'test', location_id: 2
puts "", "Documents with location:2", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
puts "(NOTE: 'D' is missing, because is not yet posted)"
articles = Article.search query: 'test b', location_id: 1
puts "", "Documents with query:B and location:1", '-'*80
articles.results.each { |a| puts "* TITLE: #{a.title}, LOCATION: #{a.author.location_id}, DATE: #{a.posted_at}" }
First, it's usually a good idea to create an isolated, extracted case like this.
In your example code, I assume you have a relationship Listing belongs_to :author. You need to properly define the mapping and serialization, which I again assume you did.
As for the query itself:
Unless you're using faceted navigation, use the filtered query, not top level filters, as in my example code.
Do not use the string query, unless you really want to expose all the power (and fragility!) of the Lucene query string query to your users.
Use the match query, as your "generic purpose" query -- Tire sprinkles some sugar on top of it, allowing to easily create multi_match queries, etc
The filter syntax in your example is correct. When the filter method is called multiple times in Tire, it creates and and filter.
Uncomment the Tire logging configuration (and possibly also the ActiveRecord logging), to see what the code is doing.

Resources