I am currently developing a small Rails 5 application where I need to pass on an ActiveRecord object to an external service based on certain events. In my model I have defined the following:
# /models/user.rb
after_create :notify_external_service_of_user_creation
def notify_external_service_of_user_creation
EventHandler.new(
event_kind: :create_user,
content: self
)
end
The EventHandler is then converting this object to JSON and is sending it through an HTTP request to the external service. By calling .to_json on the object this renders a JSON output which would look something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":12,
"another_association_id":356
}
Now, I need a way to include all first level associations directly into this, instead of just showing the foreign_key. So the construct I am looking for would be something like this:
{
"id":1234,
"email":"test#testemail.dk",
"first_name":"Thomas",
"last_name":"Anderson",
"association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
"another_association_id":{
"attr1":"some_data",
"attr2":"another_value"
},
}
My first idea was to reflect upon the Model like so: object.class.name.constantize.reflect_on_all_associations.map(&:name), where object is an instance of a user in this case, and use this list to loop over the associations and include them in the output. This seems rather tedious though, so I was wondering if there would be a better way of achieving this using Ruby 2.4 and Rails 5.
If you don't want to use an external serializer, you can override as_json for each model. as_json gets called by to_json.
module JsonWithAssociations
def as_json
json_hash = super
self.class.reflect_on_all_associations.map(&:name).each do |assoc_name|
assoc_hash = if send(assoc_name).respond_to?(:first)
send(assoc_name).try(:map, &:as_json) || []
else
send(assoc_name).as_json
end
json_hash.merge!(assoc_name.to_s => assoc_hash)
end
json_hash
end
end
You'll need to prepend this particular module so that it overrides the default as_json method.
User.prepend(JsonWithAssociations)
or
class User
prepend JsonWithAssociations
end
Related
I'm trying to integrate an algorithm I wrote in pure Ruby into a Rails app. The main class of my Ruby project could be a resource.
When it was initialized in Ruby I immediately called a function in the initialize method:
def initialize(keyword)
#keyword = keyword
#sources = get_titles_of_sources
end
In Rails, when I create new objects I usually don't have or at least see a initialize method.
#user = User.new(attribute1: value1, attribute2: value2)
But this style doesn't allow me to automatically call a method when creating a new object.
Given what you said in comments, I feel like you should avoid the callbacks (btw you should always avoid callbacks if you can...).
Anyway, I suggest you to create a build method like:
class User
def self.build(props = {})
new(props).tap do |user|
#your code on initialize
end
end
end
I usually tend to move build methods into builders. But it makes you create an additional class
Use after_initialize callback.
Lastly an after_find and after_initialize callback is triggered for
each object that is found and instantiated by a finder, with
after_initialize being triggered after new objects are instantiated as
well.
Active Record Callbacks
Sounds like a use for ActiveRecord Callbacks (http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html).
I've been working with Rails and a number of frontend js frameworks and libs, including Ember, Angular, and React. While all three libraries are powerful in their own regard, one pain point (for me at least) has always been form validation. I've always hated having to keep my model validations (in Rails) in sync with my form validations (in Ember/Angular/React).
Lately, I've been attempting to serialize a model's validators into json. However, while calling as_json on a record will give me back a json hash, it doesn't give me the type of validators for a particular attribute.
For example, let's say I have a model called Assignment. When I create a new Assignment record and call _validators on it, this is what I get.
pry(main)> Assignment.new._validators
=>
{
:title=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e123900 #attributes= [:title], #options={}>],
:full_prompt=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e122e60 #attributes=[:full_prompt], #options={}>],
:submission_window=>[#<ActiveRecord::Validations::PresenceValidator:0x0000010e1223c0 #attributes=[:submission_window], #options={}>]
}
Now here's what I get when I add on the as_json call:
pry(main)> Assignment.new._validators.as_json
=>
{
"title"=>[{"attributes"=>["title"], "options"=>{}}],
"full_prompt"=>[{"attributes"=>["full_prompt"], "options"=>{}}],
"submission_window"=>[{"attributes"=>["submission_window"], "options"=>{}}]
}
As you can see, calling as_json removes what types of validators were attached to a model's attribute.
Has anybody run into a similar situation and/or has a workaround? Thanks for any help!
Kurt
this should work better... the attributes and options hash will be a sub-hash which will be the value of a hash where the key is the validator class.. all of course themselves sub-hashes under the attributes
hash = {}
Assignment._validators.each { |k, v| v.each {|val| hash[k] ||= {}; hash[k][val.class.to_s] = val }}
hash.as_json
Rails adds a method to Object called as_json. As you can see here it checks to see if the object in question responds to to_hash (which ActiveModel::Validator does not by default).
The fallback it to invoke instance_values.as_json(options) which is how you're getting the default JSON.
I would implement the to_hash method on ActiveModel::Validator and you can put whatever information in there you'd like.
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
{} # custom implementation here...
end
end
If you want to maintain the default JSON and simply append additional information to it you could do something like
initializers/active_model_validator.rb
class ActiveModel::Validator
def to_hash
instance_values.tap do |hash|
# custom implementation here...
end
end
end
I have an instance variable in an active record class called hash_value. It's a serialized hash.
I want to display the hash as XML. Is it right to call hash_value.to_xml? Many nodes are numbers, and XML tags are not allowed to be only number (or start with a number).
I want to override the to_xml method of hash_value. I don't want to override on all hashes, just the hash that's in this record.
class ProductVersion < ActiveRecord::base
serialize :hash_value, Hash
def hash_value.to_xml
end
end
I tried the answer here redefining a single ruby method on a single instance with a lambda
but it doesn't seem to be applicable. I suspect because when I load the record, it creates a new hash_value object and thus the singleton adjustment on the original is moot.
Any thoughts would be great.
I know I could write a function hash_value_to_xml, but I'd rather avoid doing something like that.
Thanks to the first comment, I came up with a solution. Not a good one, but one that works. I'd love to see if there's a better way, because this one smells a bit.
class MyHash < Hash
def to_xml
1/0 #to see if it's run.
end
end
def hash_value
MyHash.new().merge( attributes['hash_value'] );
end
I would personally go for hash_value_to_xml route. But since you insist, here's an idea that might work (haven't tested that)
class ProductVersion < ActiveRecord::base
serialize :hash_value, Hash
alias_method :old_hash_value, :hash_value
def hash_value
h = old_hash_value
h.define_singleton_method(:to_xml) do
# your custom xml logic here
end
h
end
end
The idea is that you intercept value returned from hash_value and patch it on the fly.
I'm creating an API that accepts JSON data and I want to provide testing data for it.
Is there anything similar to factories for JSON data? I would like to have the same data available in an object and in JSON, so that I can check if import works as I intended.
JSON has strictly defined structure so I can't call FactoryGirl(:record).to_json.
In cases like this, I'll create fixture files for the JSON I want to import. Something like this can work:
json = JSON.parse(File.read("fixtures/valid_customer.json"))
customer = ImportsData.import(json)
customer.name.should eq(json["customer"]["name"])
I haven't seen something where you could use FactoryGirl to set attributes, then get it into JSON and import it. You'd likely need to create a mapper that will take your Customer object and render it in JSON, then import it.
Following Jesse's advice, in Rails 5 now you could use file_fixture (docs)
I just use a little helper for reading my json fixtures:
def json_data(filename:)
file_content = file_fixture("#{filename}.json").read
JSON.parse(file_content, symbolize_names: true)
end
Actually you can do the following with factorygirl.
factory :json_data, class: OpenStruct do
//fields
end
FactoryGirl.create(:json_data).marshal_dump.to_json
Sometime ago we implemented FactoryJSON gem that addresses this issue. It worked quite well for us so far. Readme file covers possible use cases.
Here's something that works well for me. I want to create deeply nested structures without specifying individual factories for each nesting. My usecase is stubbing external apis with webmock. Fixtures don't cut it for me since I need to stub in a variety of different data.
Define the following base factory and support code:
FactoryBot.define do
factory :json, class: OpenStruct do
skip_create # Don't try to persist the object
end
end
class JsonStrategy < FactoryBot::Strategy::Create
def result(evaluation)
super.json.to_json
end
def to_sym
:json
end
end
# Makes the function FactoryBot.json available,
# which automatically returns the hash as a json string.
FactoryBot.register_strategy(:json, JsonStrategy)
I can then define the actual factory like this:
FactoryBot.define do
factory :json_response, parent: :json do
# You can define any attributes you want here because it uses OpenStruct
ids { [] }
# This attribute will be plucked by the custom strategy. All others like
# ids above will be ignored. You can still use them here though.
json do
ids.map do |id|
{
score: 90,
data: {
id: id,
},
}
end
end
end
end
Finally you can use it like this:
FactoryBot.json(:json_response, ids: [1,2])
=> "[{\"score\":90,\"data\":{\"id\":1}},{\"score\":90,\"data\":{\"id\":2}}]"
Let's say I have a model called Article:
class Article < ActiveRecord::Base
end
And then I have a class that is intended to add behavior to an article object (a decorator):
class ArticleDecorator
def format_title
end
end
If I wanted to extend behavior of an article object, I could make ArticleDecorator a module and then call article.extend(ArticleDecorator), but I'd prefer something like this:
article = ArticleDecorator.decorate(Article.top_articles.first) # for single object
or
articles = ArticleDecorator.decorate(Article.all) # for collection of objects
How would I go about implementing this decorate method?
What exactly do you want from decorate method? Should it simply add some new methods to passed objects or it should automatically wrap methods of these objects with corresponding format methods? And why do you want ArticleDecorator to be a class and not just a module?
Updated:
Seems like solution from nathanvda is what you need, but I'd suggest a bit cleaner version:
module ArticleDecorator
def format_title
"#{title} [decorated]"
end
def self.decorate(object_or_objects_to_decorate)
object_or_objects_to_decorate.tap do |objects|
Array(objects).each { |obj| obj.extend ArticleDecorator }
end
end
end
It does the same thing, but:
Avoids checking type of the arguments relying on Kernel#Array method.
Calls Object#extend directly (it's a public method so there's no need in invoking it through send).
Object#extend includes only instance methods so we can put them right in ArticleDecorator without wrapping them with another module.
May I propose a solution which is not using Module mixins and thereby granting you more flexibility. For example, using a solution a bit more like the traditional GoF decorator, you can unwrap your Article (you can't remove a mixin if it is applied once) and it even allows you to exchange the wrapped Article for another one in runtime.
Here is my code:
class ArticleDecorator < BasicObject
def self.[](instance_or_array)
if instance_or_array.respond_to?(:to_a)
instance_or_array.map {|instance| new(instance) }
else
new(instance_or_array)
end
end
attr_accessor :wrapped_article
def initialize(wrapped_article)
#wrapped_article = wrapped_article
end
def format_title
#wrapped_article.title.upcase
end
protected
def method_missing(method, *arguments)
#wrapped_article.method(method).call(*arguments)
end
end
You can now extend a single Article by calling
extended_article = ArticleDecorator[article]
or multiple articles by calling
articles = [article_a, article_b]
extended_articles = ArticleDecorator[articles]
You can regain the original Article by calling
extended_article.wrapped_article
Or you can exchange the wrapped Article inside like this
extended_article = ArticleDecorator[article_a]
extended_article.format_title
# => "FIRST"
extended_article.wrapped_article = article_b
extended_article.format_title
# => "SECOND"
Because the ArticleDecorator extends the BasicObject class, which has almost no methods already defined, even things like #class and #object_id stay the same for the wrapped item:
article.object_id
# => 123
extended_article = ArticleDecorator[article]
extended_article.object_id
# => 123
Notice though that BasicObject exists only in Ruby 1.9 and above.
You'd extend the article class instance, call alias_method, and point it at whatever method you want (although it sounds like a module, not a class, at least right now). The new version gets the return value and processes it like normal.
In your case, sounds like you want to match up things like "format_.*" to their respective property getters.
Which part is tripping you up?
module ArticleDecorator
def format_title
"Title: #{title}"
end
end
article = Article.top_articles.first.extend(ArticleDecorator) # for single object
Should work fine.
articles = Article.all.extend(ArticleDecorator)
May also work depending on ActiveRecord support for extending a set of objects.
You may also consider using ActiveSupport::Concern.