-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GUILD-690: Initial draft of new Active Record Consumer Generator #170
base: master
Are you sure you want to change the base?
Changes from 2 commits
65c8d5d
e07ae14
8b3130d
3e41478
1222c74
1ee2615
ce5b340
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
Description: | ||
Streamlines Deimos consumer creation. Creates all the necessary components needed in a Ruby on Rails application when creating a new Active Record Consumer. | ||
|
||
Usage: | ||
bin/rails generate deimos:active_record_consumer FULL_SCHEMA_NAME [CONFIG_FILE_PATH] | ||
|
||
Options are... | ||
FULL_SCHEMA_NAME (required) Fully qualified schema name, located in config.schema.path that defines the Avro schema for the Consumer. | ||
CONFIG_FILE_PATH (optional) Path to existing config file. If not specified, defaults to config/initializers/deimos.rb. | ||
|
||
Example: | ||
bin/rails generate deimos:active_record_consumer com.test.Widget config/initializers/myconfig.config | ||
|
||
This will create: | ||
Database Migration db/migrate/20221111134112_create_widgets.rb | ||
Rails Model app/models/widget.rb | ||
Consumer Class app/lib/kafka/models/widget_consumer.rb | ||
Deimos Consumer Config config/initializers/deimos.rb or adds config to CONFIG_FILE_PATH | ||
Generated Schema Classes app/lib/schema_classes or config.schema.generated_class_path | ||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
|
||
Deimos.configure do | ||
logger Logger.new(STDOUT) | ||
# Nested config field | ||
kafka.seed_brokers ['my.kafka.broker:9092'] | ||
|
||
schema.generate_namespace_folders true | ||
schema.generated_class_path 'app/lib/schemas' | ||
|
||
# Multiple nested config fields via block | ||
consumers do | ||
session_timeout 30 | ||
offset_commit_interval 10 | ||
end | ||
|
||
# Define a consumer | ||
consumer do | ||
class_name '<%= consumer_name.classify %>' | ||
topic 'TopicToConsume' | ||
schema '<%= schema %>' | ||
namespace '<%= namespace %>' | ||
key_config field: :<%= key_field %> | ||
# include Phobos / RubyKafka configs | ||
start_from_beginning true | ||
heartbeat_interval 10 | ||
use_schema_classes true | ||
end | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
class <%= consumer_name.classify %> < Deimos::ActiveRecordConsumer | ||
record_class <%= model_name.classify %> | ||
|
||
# Optional override of the way to fetch records based on payload and | ||
# key. Default is to use the key to search the primary key of the table. | ||
# Only used in non-batch mode. | ||
def fetch_record(klass, payload, key) | ||
super | ||
end | ||
|
||
# Optional override on how to set primary key for new records. | ||
# Default is to set the class's primary key to the message's decoded key. | ||
# Only used in non-batch mode. | ||
def assign_key(record, payload, key) | ||
super | ||
end | ||
|
||
# Optional override of the default behavior, which is to call `destroy` | ||
# on the record - e.g. you can replace this with "archiving" the record | ||
# in some way. | ||
# Only used in non-batch mode. | ||
def destroy_record(record) | ||
super | ||
end | ||
|
||
# Optional override to change the attributes of the record before they | ||
# are saved. | ||
def record_attributes(payload, key) | ||
super.merge(:some_field => 'some_value') | ||
end | ||
|
||
# Optional override to change the attributes used for identifying records | ||
def record_key(payload) | ||
super | ||
end | ||
|
||
# Optional override, returns true by default. | ||
# When this method returns true, a record corresponding to the message | ||
# is created/updated. | ||
# When this method returns false, message processing is skipped and a | ||
# corresponding record will NOT be created/updated. | ||
def process_message?(payload) | ||
super | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> | ||
def up | ||
if table_exists?(:<%= table_name %>) | ||
warn "<%= table_name %> already exists, exiting" | ||
return | ||
end | ||
create_table :<%= table_name %> do |t| | ||
<%- fields.each do |key| -%> | ||
<%- next if %w(id message_id timestamp updated_at created_at).include?(key.name) -%> | ||
<%- sql_type = schema_base.sql_type(key) | ||
if %w(record array map).include?(sql_type) | ||
conn = ActiveRecord::Base.connection | ||
sql_type = conn.respond_to?(:supports_json?) && conn.supports_json? ? :json : :string | ||
end | ||
-%> | ||
t.<%= sql_type %> :<%= key.name %> | ||
<%- end -%> | ||
|
||
t.timestamps | ||
|
||
# TODO add indexes as necessary | ||
end | ||
end | ||
|
||
def down | ||
return unless table_exists?(:<%= table_name %>) | ||
drop_table :<%= table_name %> | ||
end | ||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class <%= table_name.classify %> < ApplicationRecord | ||
<%- fields.select { |f| f.enum_values.any? }.each do |field| -%> | ||
enum <%= field.name %>: {<%= field.enum_values.map { |v| "#{v}: '#{v}'"}.join(', ') %>} | ||
<% end -%> | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails/generators' | ||
require 'rails/generators/active_record/migration' | ||
require 'generators/deimos/schema_class_generator' | ||
require 'rails/version' | ||
require 'erb' | ||
|
||
# Generates a new Active Record Consumer, as well | ||
# as all the necessary files and configuration. | ||
# Streamlines the creation process into a single flow. | ||
module Deimos | ||
module Generators | ||
# Generator for ActiveRecordConsumer db migration, Rails model, Consumer, Consumer config | ||
# and Deimos Schema class | ||
class ActiveRecordConsumerGenerator < Rails::Generators::Base | ||
include Rails::Generators::Migration | ||
if Rails.version < '4' | ||
extend(ActiveRecord::Generators::Migration) | ||
else | ||
include ActiveRecord::Generators::Migration | ||
end | ||
source_root File.expand_path('active_record_consumer/templates', __dir__) | ||
|
||
argument :full_schema, desc: 'The fully qualified schema name.', required: true | ||
argument :config_path, desc: 'The path to the deimos configuration file, relative to the root directory.', required: false | ||
|
||
no_commands do | ||
|
||
# Creates database migration for creating new table | ||
def create_db_migration | ||
migration_template('migration.rb', "#{db_migrate_path}/create_#{table_name.underscore}.rb") | ||
end | ||
|
||
# Creates Rails Model | ||
def create_rails_model | ||
template('model.rb', "app/models/#{model_name}.rb") | ||
end | ||
|
||
# Creates Kafka Consumer file | ||
def create_consumer | ||
template('consumer.rb', "app/lib/kafka/models/#{consumer_name.underscore}.rb") | ||
end | ||
|
||
# Adds consumer config to config file. | ||
# Defaults to deimos.rb if config_path arg is not specified. | ||
def create_consumer_config | ||
config_file = 'deimos.rb' | ||
config_file_path = "#{initializer_path}/#{config_file}" | ||
config_file_path = config_path if config_path.present? | ||
|
||
if File.exist?(config_file_path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should validate that the config file exists if it's passed in. If not, we can create it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I tested it by passing in a config file path that did not already exist, it did go ahead and create that config file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, that was unclear. We should only create it if no path was specified. If a path was specified and doesn't exist, we should assume that means that they made a mistake and raise an error. Actually, I'm not sure we need to create it at all. If we want to auto-create a config file, it probably would be in an installer or something, but I don't think we have a use case for that. So maybe I'd just say don't create anything, and throw an error if the file doesn't exist. |
||
config_template = File.expand_path(find_in_source_paths('config.rb')) | ||
insert_into_file(config_file_path.to_s, CapturableERB.new(::File.binread(config_template)).result(binding)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't this insert the whole file? We need a way to just insert the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I will change it to just insert the |
||
else | ||
template('config.rb', config_file_path.to_s) | ||
end | ||
end | ||
|
||
# Generates schema classes | ||
def create_deimos_schema_class | ||
Deimos::Generators::SchemaClassGenerator.start | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be a way to generate just the one schema class we care about. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which is the one schema class that we care about? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The one that was passed in as an input. |
||
end | ||
|
||
# @return [String] | ||
def db_migrate_path | ||
if defined?(Rails.application) && Rails.application | ||
paths = Rails.application.config.paths['db/migrate'] | ||
paths.respond_to?(:to_ary) ? paths.to_ary.first : paths.to_a.first | ||
else | ||
'db/migrate' | ||
end | ||
end | ||
|
||
# @return [String] | ||
def migration_version | ||
"[#{ActiveRecord::Migration.current_version}]" | ||
rescue StandardError | ||
'' | ||
end | ||
|
||
# @return [String] | ||
def table_class | ||
table_name.classify | ||
end | ||
|
||
# @return [String] | ||
def schema | ||
last_dot = self.full_schema.rindex('.') | ||
self.full_schema[last_dot + 1..-1] | ||
end | ||
|
||
# @return [String] | ||
def namespace | ||
last_dot = self.full_schema.rindex('.') | ||
self.full_schema[0...last_dot] | ||
end | ||
|
||
# @return [Deimos::SchemaBackends::Base] | ||
def schema_base | ||
@schema_base ||= Deimos.schema_backend_class.new(schema: schema, namespace: namespace) | ||
end | ||
|
||
# @return [Array<SchemaField>] | ||
def fields | ||
schema_base.schema_fields | ||
end | ||
|
||
# @return [String] | ||
def table_name | ||
schema.tableize | ||
end | ||
|
||
# @return [String] | ||
def model_name | ||
table_name.underscore.singularize | ||
end | ||
|
||
# @return [String] | ||
def consumer_name | ||
"#{schema.classify}Consumer" | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A lot of this is copied from the migration template. There should be a way to just call the migration generator from this generator rather than having the same code twice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered extending from the migration template but decided it wasn't the right approach. Will find a better way! |
||
|
||
# @return [String] | ||
def initializer_path | ||
if defined?(Rails.application) && Rails.application | ||
paths = Rails.application.config.paths['config/initializers'] | ||
paths.respond_to?(:to_ary) ? paths.to_ary.first : paths.to_a.first | ||
else | ||
'config/initializers' | ||
end | ||
end | ||
|
||
# @return [String] Returns the name of the first field in the schema, as the key | ||
def key_field | ||
fields.first.name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is almost definitely not going to be the right approach. It probably makes more sense to just have the key config specified as part of the input. We'll also need the key schema (if there is one) when generating the schema classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, yes makes sense to include key config as an input. In terms of the key schema, how does that work? I noticed that the schema glasses generator doesn't take in any arguments. So, if the user has a key schema for the new consumer, do they just have to place it in the SCHEMA_ROOT prior to running the ActiveRecordConsumer generator? And then what will be the difference in output? Will the generated schema classes be different? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The generator reads the configuration which already knows what the key schema is. The generator has a method |
||
end | ||
end | ||
|
||
# desc 'Generate necessary files and configuration for a new Active Record Consumer.' | ||
# @return [void] | ||
def generate | ||
create_db_migration | ||
create_rails_model | ||
create_consumer | ||
create_consumer_config | ||
create_deimos_schema_class | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we already have this template file? We should be able to reuse it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - better idea to reuse the existing code - will fix this.