mongoid document to_json including all embedded documents without ':include' on each one - ruby

Given an arbitrary mongoid document how do i convert it to JSON and include any embedded structures without specifically including those structures in my to_json statement.
For example:
#!/usr/bin/env ruby
require 'mongoid'
require 'json'
require 'pp'
class Doc
include Mongoid::Document
include Mongoid::Timestamps
field :doc_specific_info , type: String
embeds_many :persons
end
class Person
include Mongoid::Document
field :role , type: String
field :full_name , type: String
embeds_many :addresses
embedded_in :Doc
end
class Address
include Mongoid::Document
field :full_address , type: String
end
doc = Doc.new
doc.doc_specific_info = "TestReport"
p = Person.new
p.role = 'buyer'
p.full_name = 'JOHN DOE'
doc.persons << p
a = Address.new
a.full_address = '1234 nowhere ville'
doc.persons.first.addresses << a
# THIS STATEMENT
pp JSON.parse(doc.to_json(:include => { :persons => { :include => :addresses } } ) )
# GIVES ME
# {"_id"=>"4ee0d30fab1b5c5743000001",
# "created_at"=>nil,
# "doc_specific_info"=>"TestReport",
# "updated_at"=>nil,
# "persons"=>
# [{"_id"=>"4ee0d30fab1b5c5743000002",
# "full_name"=>"JOHN DOE",
# "role"=>"buyer",
# "addresses"=>
# [{"_id"=>"4ee0d30fab1b5c5743000003",
# "full_address"=>"1234 nowhere ville"}]}]}
# THIS STATEMENT
pp JSON.parse(doc.to_json() )
# GIVES ME
# {"_id"=>"4ee0d2f8ab1b5c573f000001",
# "created_at"=>nil,
# "doc_specific_info"=>"TestReport",
# "updated_at"=>nil}
So what I want is a statement something like this:
# FOR A STATEMENT LIKE THIS
pp JSON.parse(doc.to_json( :everything } ) )
# TO GIVE ME THE COMPLETE DOCUMENT LIKE SO:
# {"_id"=>"4ee0d30fab1b5c5743000001",
# "created_at"=>nil,
# "doc_specific_info"=>"TestReport",
# "updated_at"=>nil,
# "persons"=>
# [{"_id"=>"4ee0d30fab1b5c5743000002",
# "full_name"=>"JOHN DOE",
# "role"=>"buyer",
# "addresses"=>
# [{"_id"=>"4ee0d30fab1b5c5743000003",
# "full_address"=>"1234 nowhere ville"}]}]}
Does such a statement exist? If not then is my only alternative recusing the structure of the document and producing the proper includes myself? If there is another way to visualize the whole document that would be better?

This was answered by rubish in the forum but he didn't post an answer so I am doing that.
The answer is to use "doc.as_document.as_json" which will give you the whole document.
pp doc.as_document.as_json

You can override the #to_json method in your document to add all include.
class Person
def to_json(*args)
super(args.merge({:include => { :persons => { :include => :addresses } } } )
end
end
Now you can have all by doing
person.to_json()
If you want return the complete with only :everything option you can do :
class Person
def to_json(*args)
if args[0] == :everything
super(args.merge({:include => { :persons => { :include => :addresses } } } )
else
super(args)
end
end
end

Related

How to test "initialize" method using RSpec

github_asset.rb
# frozen_string_literal: true
require 'asset_ingester/helpers/project_details'
require 'active_model'
module AssetIngester
module Asset
class GithubAsset
include ActiveModel::Serializers::JSON
attr_reader :id, :name, :full_name, :description, :owner_name, :owner_url,
:owner_avatar_url, :url, :html_url, :artifact_id, :jiras, :asset_type
# Public: Initializes an instance of the GithubAsset class
#
# repo - A hash containing github repository details
# asset_type - A string representation of the asset type
def initialize(repo, asset_type)
#id = repo[:id]
#name = repo[:name]
#full_name = repo[:full_name]
#description = repo[:description]
#owner_name = repo.dig(:owner, :login)
#owner_url = repo.dig(:owner, :url)
#owner_avatar_url = repo.dig(:owner, :avatar_url)
#url = repo[:url]
#html_url = repo[:html_url]
#asset_type = asset_type
#artifact_id = repo[:artifact_id] if repo[:artifact_id] && !repo[:artifact_id].empty?
#jiras = repo[:jiras] if repo[:jiras] && !repo[:jiras].empty?
end
# Public: Defines the JSON serialization structure
#
# https://edgeguides.rubyonrails.org/active_model_basics.html#serialization
def attributes
{
'id' => #id,
'name' => #name,
'full_name' => #full_name,
'description' => #description,
'owner_name' => #owner_name,
'owner_url' => #owner_url,
'owner_avatar_url' => #owner_avatar_url,
'url' => #url,
'html_url' => #html_url,
'asset_type' => #asset_type,
'artifact_id' => #artifact_id,
'jiras' => #jiras
}.compact
end
end
end
end
github_asset_spec.rb
require 'asset_ingester/asset/github_asset'
RSpec.describe AssetIngester::Asset::GithubAsset, type: :api do
context "creating" do
let(:asset_type) {"node_package"}
let(:repo) do
[id: 131_690,
name: 'acm-care-management-js',
full_name: 'AcuteCaseManagementUI/acm-care-management-js',
owner_name: 'AcuteCaseManagementUI',
owner_url: 'https://github.cerner.com/api/v3/users/AcuteCaseManagementUI',
owner_avatar_url: 'https://avatars.github.cerner.com/u/4095?',
url: 'https://github.cerner.com/api/v3/repos/AcuteCaseManagementUI/acm-care-management-js',
html_url: 'https://github.cerner.com/AcuteCaseManagementUI/acm-care-management-js',
asset_type: 'node_package',
artifact_id: "",
jiras: [] ]
end
describe '::attributes' do
subject { AssetIngester::Asset::GithubAsset.attributes(repo, asset_type) }
it 'instantiates the class with 2 arguments' do
expect(subject).to be_an_instance_of(AssetIngester::Asset::GithubAsset)
end
it 'sets a to the first argument' do
expect(subject.repo).to eq(repo)
end
it 'sets b to the second argument' do
expect(subject.asset_type).to eq(asset_type)
end
end
end
end
This is how i tried testing the github_asset.rb file but however I'am receiving the following error while defining the subject
AssetIngester::Asset::GithubAsset creating ::attributes instantiates the class with 2 arguments
Failure/Error: subject { AssetIngester::Asset::GithubAsset.attributes(repo, asset_type) }
NoMethodError:
undefined method `attributes' for AssetIngester::Asset::GithubAsset:Class
Did you mean? attr_writer
I am green to RSpec testing and want to know how this can be done.
You get that NoMethodError because you are trying to call attributes as a class method:
subject { AssetIngester::Asset::GithubAsset.attributes(repo, asset_type) }
# ^^^^^^^^^^
although in your code, attributes is defined as an instance method.
But apart from that, you ask "how ruby initialize method can be tested" and apparently your test code is all about initialize and not about attributes, so let's start at the beginning:
describe '::attributes' do
# ...
end
You want to test initialize, so attributes should be initialize. And since initialize is an instance method (rather than a class method), :: should be #:
describe '#initialize' do
# ...
end
And your subject should be an instance of GithubAsset which is created via new1:
describe '#initialize' do
subject { AssetIngester::Asset::GithubAsset.new(repo, asset_type) }
# ...
end
With that in place, you can start writing your tests, e.g.:
describe '#initialize' do
subject { AssetIngester::Asset::GithubAsset.new(repo, asset_type) }
it 'sets the id attribute' do
expect(subject.id).to eq(131690)
end
end
1 In Ruby, you rarely invoke ::allocate and #initialize directly. So instead of:
obj = Array.allocate
obj.send(:initialize, 5) { |i| i ** 2 }
obj #=> [0, 1, 4, 9, 16]
you typically just call new and let it call allocate and initialize for you:
obj = Array.new(5) { |i| i ** 2 }
obj #=> [0, 1, 4, 9, 16]

How to filter by foreign id and local attribute via belongs_to?

The following models are linked via belongs_to:
require 'mongoid'
class Sensor
include Mongoid::Document
field :sensor_id, type: String
validates_uniqueness_of :sensor_id
end
...
require 'mongoid'
require_relative 'sensor.rb'
class SensorData
include Mongoid::Document
belongs_to :sensor
field :date, type: Date
field :ozonMax1h, type: Float
field :ozonMax8hMittel, type: Float
index({ date: 1, sensor_id: 1 }, { unique: true })
end
Here is a Sinatra app which provides a few API paths based on these models:
require 'sinatra'
require 'csv'
require_relative './models/sensor.rb'
require_relative './models/sensor_data.rb'
configure do
Mongoid.load!('./mongoid.yml')
end
def prepare_for_export(sensor_data)
converted_data = sensor_data.asc(:date).map do |e|
{
sensor_id: e.sensor.nil? ? :null : e.sensor.sensor_id,
date: e.date,
ozonMax1h: e.ozonMax1h,
ozonMax8hMittel: e.ozonMax8hMittel
}
end
converted_data
end
def convert_to_json(sensor_data)
prepare_for_export(sensor_data).to_json
end
def convert_to_csv(sensor_data)
data = prepare_for_export sensor_data
csv_string = CSV.generate do |csv|
csv << data.first.keys
data.each do |hash|
csv << hash.values
end
end
csv_string
end
def get_recent
max_date = SensorData.max(:date)
SensorData.where(date: max_date)
end
def get_for_year(year)
SensorData.where(:date.gte => Date.new(year, 1, 1)).where(:date.lte => Date.new(year, 12, 31))
end
def get_for_sensor(sensor)
foo = SensorData.where(sensor_id: sensor)
puts "hallo"
return foo
end
get '/api/v1/stations' do
content_type :json
Sensor.all.map { |e| {sensor_id: e.sensor_id} }.to_json
end
get '/api/v1/sensordata/:year' do
content_type :json
convert_to_json get_for_year(params[:year].to_i)
end
get '/api/v1/sensordata/:year/csv' do
convert_to_csv get_for_year(params[:year].to_i)
end
get '/api/v1/recent' do
content_type :json
convert_to_json get_recent
end
I would like to output the SensorData for a particular sensor such as here:
/api/v1/stations/:sensor_id/sensordata/:year/csv
I am not sure what you are trying to do or even if you are still looking for an answer but here it goes. Something seems wrong with the models in the example you have here. Sounds like part of what you are doing would work if Sensor knows about sensor_data. So might need to add this to Sensor class:
has_many :sensor_data
Though the singular of data is datum. The class would be expected to be SensorDatum. If you can't change it, you need to tell Mongoid the class_name to expect in the has_many is actuall SensorData.
You CAN specify foreign_key in Mongoid with belongs_to.
You CANNOT filter with the belongs_to like you can with ActiveRecord, but you can use scopes outside of the belongs_to to get the same effect. Exampe:
belongs_to :sensor
scope :for_year, -> (year) { where(:date.gte => Date.new(2015,1,1)).where(:date.lte => Date.new(2015, 12, 31))}
or
belongs_to :sensor
def self.for_year year
where(:date.gte => Date.new(year,1,1)).where(:date.lte => Date.new(year, 12, 31))
end
So your query would become something like this:
sensor = Sensor.find_by(sensor_id: params[:sensor_id])
sensor.sensor_data.for_year(2015)

Multiple Route Params in Grape

How do you get multiple route params in Grape to work in grape?
I can make this route work:
.../api/company/:cid
But when I try this:
.../api/company/:cid/members
.../api/company/:cid/members/:mid
I get errors.
Here's the code that works.
resource 'company' do
params do
optional :start_date, type: Date, desc: "Start date of range."
optional :end_date, type: Date, desc: "End date of range."
end
route_param :cid do
get do
{company_id: params[:cid]}
end
end
class API::Company < Grape::API
resource :company do
route_param :cid do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid
# process get
end
resources :members do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid/members/'
# process get
end
route_param :mid do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid/members/:mid'
# process get
end
end
end
end
end
end
You can do that that way. Or you can create two different resources file and mount Members to Company. Like so:
# api/company.rb
class API::Company < Grape::API
resource :company do
route_param :cid do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid
# process get
end
mount API::Members
end
end
end
# api/member.rb
class API::Member < Grape::API
resources :members do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid/members/'
# process get
end
route_param :mid do
desc "GET"
params do
# your params
end
get '/' do # => '.../api/company/:cid/members/:mid'
# process get
end
end
end
Hope that helps
Another way to do that is using a regexp to validate the ids.
resource 'company' :requirements => { :id => /[0-9]*/, :mid => /[0-9]*/ } do
get '/:id' do
# returns company
end
get ":id/members" do
members = Company.find_by_id(params[:id]).members
present members, :with => Members::MemberResponseEntity
end
get ":id/members/:mid" do
member = Member.find_by_id(params[:mid])
present member, :with => Members::MemberResponseEntity
end
end

How can I reload the table schema in sequel?

Given I have the following migration:
Sequel.migration do
up do
alter_table :users do
add_column :is_admin, :default => false
end
# Sequel runs a DESCRIBE table statement, when the model is loaded.
# At this point, it does not know that users have a is_admin flag.
# So it fails.
#user = User.find(:email => "admin#fancy-startup.example")
#user.is_admin = true
#user.save!
end
end
Then sequel does not automatically reload the table structure (see comment inline).
I am using this ugly hack to work around it:
# deep magic begins here. If you remove a single line, it will
# break the migration.
User.db.schema("users", :reload => true)
User.instance_variable_set(:#db_schema, nil)
User.columns
User.new.respond_to?(:is_admin=)
sleep 1
Is there a better way?
Much simpler than your hack is this hack: (re)set the dataset to the table name:
User.set_dataset :users
Seen in action:
require 'sequel'
DB = Sequel.sqlite
DB.create_table :users do
primary_key :id
String :name
end
class User < Sequel::Model; end
User << { name:"Bob" }
DB.alter_table :users do
add_column :is_admin, :boolean, default:false
end
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>false}>
p User.setter_methods #=> ["name="]
User.set_dataset :users # Make the magic happen
p User.setter_methods #=> ["name=", "is_admin="]
#user = User.first
#user.is_admin = true
#user.save
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>true}>
Note that there is no Sequel::Model#save! method; I changed it to save so that it would work.

How to save received string parameters in array field?

How to extract and save array from string parameter? I'm trying convert string beafore_create but this doesn't work. When I comment before_create :waypoints Mongoid throw error:
Parameters: {
"utf8"=>"✓",
"authenticity_token"=>"nehoT1fnza/ZW4XB4v27uZsfFjjOu/ucIhzMmMKgWPo=",
"trip"=>{
"title"=>"test",
"description"=>"test",
"waypoints"=>"[[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]"
}
}
Completed 500 Internal Server Error in 1ms
Mongoid::Errors::InvalidType (Field was defined as a(n) Array, but received a String with the value "[[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]".):
EDIT Thanks for help, now it work but I don't know whether following approach is good. I remove before_create and change parameter name from waypoints to waypoints_s and def waypoints to def waypoints_s:
#Parameters:
#"waypoints"=>"[[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]"
"waypoints_s"=>"[[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]"
class Trip
include Mongoid::Document
field :title, :type => String
field :description, :type => String
field :waypoints, :type => Array
#before_create :waypoints
#def waypoints=(arg)
def waypoints_s=(arg)
if (arg.is_a? Array)
##waypoints = arg
self.waypoints = arg
elsif (arg.is_a? String)
##waypoints = arg.split(',')
self.waypoints = JSON.parse(arg)
else
return false
end
end
end
class TripsController < ApplicationController
def create
#trip = Trip.create(params[:trip])
#trip.save
end
end
Parse the string as a JSON object:
require 'json'
waypoints = "[[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]"
JSON.parse(waypoints)
=> [[52.40637, 16.92517], [52.40601, 16.925040000000003], [52.405750000000005, 16.92493], [52.40514, 16.92463], [52.404320000000006, 16.924200000000003]]
You need to use serialize http://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-serialize
This method serialize your object to database by YAML format (let's say just text with some format).
class Trip < ActiveRecord::Base
serialize :waypoints
end
trip = Trip.create( :waypoints => [[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]])
Trip.find(trip.id).waypoints # => [[52.40637,16.92517],[52.40601,16.925040000000003],[52.405750000000005,16.92493],[52.40514,16.92463],[52.404320000000006,16.924200000000003]]

Resources