I want to develop a rubygem which is intended to be installed in a rails application.
The gem will contain few models and their database migrations.
Also I would like to add tests asserting the models and their relationships. I prefer RSpec for doing that.
While I was about to start I got stuck with a question that how to use ActivRecord in a gem so that using the tests I can insert the fixture data and test the relationships and behaviour.I believe SQLite should prove to be the best option for database here.
Note: I haven't developed any Rubygem before and this will be the first one I am attempting. Thus any help will be highly appreciated to guide me in the right direction.
Thanks.
Update on Jul 30, 2018
I found a similar question Ruby Gem Development - How to use ActiveRecord? which is exactly what I want to do. But the answers are not quite clear. Hope this helps in understanding my question.
You can get the functionality you are looking for using generators. Basically you write templates for the files that the user should have (models, migrations, tests) and save them with your gemfile. You then allow the user to copy those files in using commands.
This is a good link that goes into much more detail on generators, but here is a small example that I used in one of my gems:
gem_name/lib/generators/gem_name/install_generator.rb
module GemName
class InstallGenerator < Rails::Generators::Base
include Rails::Generators::Migration
# Allow user to specify a different model name
argument :user_class, type: :string, default: "User"
# Templates to copy
source_root File.expand_path('../../../templates', __FILE__)
# Copy initializer into user app
def copy_initializer
copy_file('create_initializer.rb', 'config/initializers/gem_name.rb')
end
# Copy user information (model & Migrations) into user app
def create_user_model
fname = "app/models/#{user_class.underscore}.rb"
unless File.exist?(File.join(destination_root, fname))
template("user_model.rb", fname)
else
say_status('skipped', "Model #{user_class.underscore} already exists")
end
end
# Copy migrations
def copy_migrations
if self.class.migration_exists?('db/migrate', "create_gem_name_#{user_class.underscore}")
say_status('skipped', "Migration create_gem_name_#{user_class.underscore} already exists")
else
migration_template('create_gem_name_users.rb.erb', "db/migrate/create_gem_name_#{user_class.pluralize.underscore}.rb")
end
end
private
# Use to assign migration time otherwise generator will error
def self.next_migration_number(dir)
Time.now.utc.strftime("%Y%m%d%H%M%S")
end
end
end
Finally, just some advice/personal opinion; your gem should either run under a variety of test suites and databases or else you should indicate that you only support that setup and assume they are available in the user's project. Though I think you could shoehorn in a second test suite, trying to force a second database wouldn't be possible, also you don't need to worry about the DB using migrations unless you want to use a data type that isn't available in all the supported DBs.
A better approach, in my opinion, would be to write separate generators for the specs you want and let the user run them optionally. This will copy the tests in and allow them to modify if they like.
Related
I'm currently developing three Rails applications that are going to use the same database and and the same models. In order to keep them in sync, I want to move these models into an independent gem.
I've read the RubyGems guide for creating my own gem but I'm not sure of how some things should be handled (e.g. database credentials (database.yml), tests for the models, migrations...)
Any thoughts on best practices or pointers to existing gems providing this behavior?
The migrations go in your gem.
You can cause them to be included in your containing app by putting this in your gem's engine.rb file (when using Rails::Engine):
'lib/your_gem/engine.rb'
module YourGem
class Engine < ::Rails::Engine
initializer :append_migrations do |app|
unless app.root.to_s.match root.to_s
config.paths["db/migrate"].expanded.each do |expanded_path|
app.config.paths["db/migrate"] << expanded_path
end
end
end
end
end
You don't have to use Rails::Engine (in fact, it comes with some unneeded overhead). But, if you're doing this for the first time, it may be the easiest thing to do.
schema.rb will be auto-generated in your containing app's db directory as is normally the case when you run rake db:migrate.
I'd like to use a database within a Ruby gem that I'm writing. The gem is meant to be used within Rails applications, and will contain an inverted index of documents passed in from the main Rails app.
I'm a bit confused as to how to go about this. Should I hook into the main Rails database somehow? Or should I have a standalone database? Ideally I'd just like to use ActiveRecord to create, update, delete and query entries but I am not sure how I'd set this up.
Data would go into the database at this point:
module ActiveRecordExtension
extend ActiveSupport::Concern
class_methods do
def foo
"bar"
end
end
included do
after_save :add_to_inverted_index
end
def add_to_inverted_index
# This is where I'd take the fields from the Rails app
# and include them to my inverted index. However, I'm struggling
# to find a way to hook into a database from my gem to do this.
end
end
# Include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
Suggestions are much appreciated! Thanks
Well after your clarification, you should use main Rails database. Just create a migration and insert the table(s) you need. You should do that because:
Everyone that uses your gem will know what is stored at the database.
With migrations, you can easily rollback the migration, making it simple to reverse something, if needed.
There's no need to create extra dependencies. Imagine a project that you did in RoR and think if the mess it would be if every gem that you used created its own database.
Maybe you should take a look at known gems and how they do that. I'm thinking about Devise.
I have a model in my rails application which is
class Person < ActiveRecord::Base
..
end
I would like to access this model from a different ruby file for certain manipulations. This is to populate my db. Can somebody please tell me how to go about it?
I am new to ruby on rails and hence a trivial question. I tried including the model by using require or require relative but I get a LoadError
If you use a rake task to populate/manipulate your database then you can solve this problem by making the task depend on the rails environment, this will set up everything you need to access the db:
task :my_task => :environment do
#do whatever with your models here
end
I am trying to generate a resource and I have removed all references to Active_record and removed the databse.yml file.
The rails server starts ok but when I try to generate a model:
rails g resource contact
I get the following error:
No value provided for required options '--orm'
Is there a way to specify no database when generating a resource?
There isn't an easy way, no. If you look at the source code for the resource generator, you'll see this part regarding the orm:
# Loads the ORM::Generators::ActiveModel class. This class is responsible
# to tell scaffold entities how to generate an specific method for the
# ORM. Check Rails::Generators::ActiveModel for more information.
def orm_class
#orm_class ||= begin
# Raise an error if the class_option :orm was not defined.
unless self.class.class_options[:orm]
raise "You need to have :orm as class option to invoke orm_class and orm_instance"
end
begin
"#{options[:orm].to_s.camelize}::Generators::ActiveModel".constantize
rescue NameError
Rails::Generators::ActiveModel
end
end
end
So it explicitly rejects any attempt to run this command without an ORM, and if you do specify an ORM, it's looking for ORM::Generators::ActiveModel. And in the comments at the top, it specifies a location to find more information, Rails::Generators::ActiveModel. The comments at the top there explain how to extend it to create an ORM specification.
The only one built-in to rails by default is the ActiveRecord generator.
There is a gem called rails3-generators that includes generators for a number of common libraries, but you can see that for ORMs it only adds functionality for data_mapper, mongo_mapper, mongoid, and active_model.
As far as I know, there is no pre-built ORM generator for "no database". You could write one yourself, if you want, by following the instructions at the top of Rails::Generators::ActiveModel (and using the rails3-generators gem source as a reference of you need it).
But if that seems like too much effort, I'd recommend just telling it to generate using the built-in ActiveRecord generator, and then just manually modifying/removing anything it generated related to that ORM.
I was wondering how you were testing the search in your application when using ElasticSearch and Tire.
How do you setup a new ElasticSearch test instance? Is there a way to mock it?
Any gems you know of that might help with that?
Some stuff I found helpful:
I found a great article answering pretty much all my questions :)
http://bitsandbit.es/post/11295134047/unit-testing-with-tire-and-elastic-search#disqus_thread
Plus, there is an answer from Karmi, Tire author.
This is useful as well: https://github.com/karmi/tire/wiki/Integration-Testing-Rails-Models-with-Tire
I can't believe I did not find these before asking...
Prefixing your index-names for the current environment
You could set a different index-name for each environment (in your case: the test environment).
For example, you could create an initializer in
config/initializers/tire.rb
with the following line:
Tire::Model::Search.index_prefix "#{Rails.application.class.parent_name.downcase}_#{Rails.env.to_s.downcase}"
A conceivable approach for deleting the indexes
Assuming that you have models named Customer, Order and Product, put the following code somewhere at your test-startup/before-block/each-run-block.
# iterate over the model types
# there are also ways to fetch all model classes of the rails app automaticly, e.g.:
# http://stackoverflow.com/questions/516579/is-there-a-way-to-get-a-collection-of-all-the-models-in-your-rails-app
[Customer, Order, Product].each do |klass|
# make sure that the current model is using tire
if klass.respond_to? :tire
# delete the index for the current model
klass.tire.index.delete
# the mapping definition must get executed again. for that, we reload the model class.
load File.expand_path("../../app/models/#{klass.name.downcase}.rb", __FILE__)
end
end
Alternative
An alternative could be to set up a different ElasticSearch instance for testing on another port, let's say 1234.
In your enviornment/test.rb you could then set
Tire::Configuration.url "http://localhost:1234"
And at a suitable location (e.g. your testing startup) you can then delete all indexes on the ElasticSearch testing-instance with:
Tire::Configuration.client.delete(Tire::Configuration.url)
Maybe you must still make sure that your Tire-Mapping definitions for you model classes are still getting called.
I ran into a quirky bug when deleting my elasticsearch index via tire in my rspec suite. In my Rspec configuration, similar to the Bits and Bytes blog, I have an after_each call which cleans the database and wipes out the index.
I found I needed to call Tire's create_elasticsearch_index method which is responsible for reading the mapping in the ActiveRecord class to set up the appropriate analyzers, etc. The issue I was seeing was I had some :not_analyzed fields in my model which were actually getting analyzed (this broke how I wanted faceting to work).
Everything was fine on dev, but the test suite was failing as facets were being broken down by individual words and not the entire multi word string. It seems that the mapping configuration was not being created appropriately in rspec after the index was deleted. Adding the create_elasticsearch_index call fixed the problem:
config.after(:each) do
DatabaseCleaner.clean
Media.tire.index.delete
Media.tire.create_elasticsearch_index
end
Media is my model class.
I ran into similar issues and here's how I solved it. Bare in mind that my solution builds on top of #spaudanjo solution. Since I'm using spork, I add this inside the spec_helper.rb's Spork.each_run block, but you may add this into any other each/before block.
# Define random prefix to prevent indexes from clashing
Tire::Model::Search.index_prefix "#{Rails.application.class.parent_name.downcase}_#{Rails.env.to_s.downcase}_#{rand(1000000)}"
# In order to know what all of the models are, we need to load all of them
Dir["#{Rails.root}/app/models/**/*.rb"].each do |model|
load model
end
# Refresh Elastic Search indexes
# NOTE: relies on all app/models/**/*.rb to be loaded
models = ActiveRecord::Base.subclasses.collect { |type| type.name }.sort
models.each do |klass|
# make sure that the current model is using tire
if klass.respond_to? :tire
# delete the index for the current model
klass.tire.index.delete
# the mapping definition must get executed again. for that, we reload the model class.
load File.expand_path("../../app/models/#{klass.name.downcase}.rb", __FILE__)
end
end
It basically defines it's own unique prefix for every test case so that there are no in indexes. The other solutions all suffered from a problem where even after deleting the index, Elastic Search wouldn't refresh the indexes (even after running Model.index.refresh) which is why the randomized prefix is there.
It also loads every model and checks if it responds to tire so that we no longer need to maintain a list of all of the models that respond to tire both in spec_helper.rb and in other areas.
As this method doesn't "delete" the indexes after using it, you will have to manually delete it on a regular basis. Though I don't imagine this to be a huge issue, you can delete with the following command:
curl -XDELETE 'http://localhost:9200/YOURRAILSNAMEHERE_test_*/'
To find what YOURRAILSNAMEHERE is, run rails console and run Rails.application.class.parent_name.downcase. The output will be your project's name.