Keith Gaddis ( @karmajunkie)
http://karmajunkie.com [email protected]
I like to:
- hack on stuff.
- fix stuff.
- make businesses work better.
class Event < ActiveRecord::Base
include BatchTouchable
belongs_to :round
has_one :chapter, through: :round
has_one :chapter_season, through: :round
has_many :event_invitations, dependent: :delete_all
has_many :pnms, :through => :event_invitations, :source => :pnm
delegate :voting_scheme, to: :round
def invited?(pnm)
event_invitations.where(:pnm_id => pnm.id).exists?
end
end
defmodule Bling.Event do
@moduledoc """
Originally imported from Event
"""
use Bling.Web, :model
schema "events" do
belongs_to :round, Bling.Round
has_many :event_invitations, Bling.EventInvitation
field :external_id, :string
field :name, :string
field :description, :string
field :chapter_id, :integer
field :start_at, Ecto.DateTime
field :end_at, Ecto.DateTime
field :university_id, :integer
field :status, :string
field :open_for_voting, :boolean
field :created_at, Ecto.DateTime
field :updated_at, Ecto.DateTime
end
end
require 'fileutils'
TEMPLATE= <<-END_TEMPLATE
defmodule <%= mod %>.<%= model.to_ecto_model_name %> do
@moduledoc """
Originally imported from <%=model.name %>
"""
use <%= mod %>.Web, :model
schema "<%= model.table_name %>" do
<% model.associations.reject(&:through?).each do |assoc| %> <%= assoc.to_declaration %>
<% end %>
<% model.fields.each do |field| %> <%= field.to_declaration %>
<% end %>
end
end
END_TEMPLATE
class Association
attr_reader :assoc
def initialize(mod, assoc)
@mod=mod
@assoc=assoc
end
def <=>(other)
begin
if through?
if through_klass == other.terminal_association
1
else
-1
end
else
0
end
rescue Exception => e
debugger
0
end
end
def through?
@assoc.options[:through]
end
def foreign_key
if through?
@assoc.through_reflection.foreign_key
else
@assoc.association_foreign_key
end
end
def through_assoc
@ta ||= @assoc.through_reflection.macro.to_s.classify.constantize.new(@mod, @assoc.through_reflection)
end
def terminal_association
if through?
through_assoc.terminal_association
else
self
end
end
def macro
@assoc.macro
end
def to_declaration
"#{@assoc.macro} :#{@assoc.name}, #{@mod}.#{to_ecto_model_name}"
end
def to_ecto_model_name
if @assoc.klass
@assoc.klass.name.gsub("::", ".")
end
end
end
class BelongsTo < Association
def foreign_key
@assoc.foreign_key
end
end
class HasMany < Association
def source_name
@assoc.source_reflection.name
rescue Exception => e
@assoc.name
end
def through_klass
@assoc.through_reflection.klass
end
def to_declaration
if through?
[through_assoc.to_declaration, "has_many :#{@assoc.name}, through: [:#{@assoc.options[:through]}, :#{source_name}]"].join("\n")
else
super
end
end
def to_ecto_model_name
if @assoc.options.has_key?(:through)
through_source=through_klass.reflections[@assoc.name.to_s.singularize.to_sym].klass.name.gsub("::", ".")
else
super
end
end
end
class HasOne < Association
def source_name
@assoc.source_reflection.name
rescue Exception => e
@assoc.name
end
def through_klass
@assoc.through_reflection.klass
end
def to_ecto_model_name
if @assoc.options.has_key?(:through)
[email protected]_reflection.klass.reflections[@assoc.name].klass.name.gsub("::", ".")
through_source
else
super
end
end
def to_declaration
if through?
[
through_assoc.to_declaration,
"has_one :#{@assoc.name}, through: [:#{@assoc.options[:through]}, :#{source_name}]"
].join("\n")
else
super
end
end
end
class HasAndBelongsToMany < Association
end
class PolymorphicAssoc < Association
def initialize(mod, assoc)
@mod=mod
@assoc=assoc
end
def to_declaration
"# polymorphic associations are tricky so we're not enabling this yet\n##{@assoc.macro} :#{@assoc.name.to_s}, #{@mod}.#{self.to_ecto_model_name}"
end
def name
@assoc.name.to_s
end
def to_ecto_model_name
@assoc.name.to_s.classify.gsub("::", ".")
end
end
class Field
def initialize(f)
@field=f
end
def type_label
if ['json', 'jsonb'].include?(@field.sql_type )
':map'
else
":#{@field.type.to_s}"
end
end
def to_declaration
"field :#{@field.name}, #{type_label}"
end
end
class TextField < Field
def type_label
":string"
end
end
class UuidField < Field
def type_label
"Ecto.UUID"
end
end
class InetField < Field
def type_label
":string"
end
end
class DateField < Field
def type_label
"Ecto.Date"
end
end
class DatetimeField < Field
def type_label
"Ecto.DateTime"
end
end
class ModelWrapper
attr_accessor :klass
def initialize(mod, klass)
@mod=mod
@klass=klass
end
def to_ecto_model_name
@klass.name.gsub("::", ".")
end
def table_name
@klass.table_name
end
def name
@klass.name
end
def associations
@assocs=klass.reflect_on_all_associations.map{ |a| a.polymorphic? ? ::PolymorphicAssoc.new(@mod, a) : a.macro.to_s.classify.constantize.new(@mod, a) }
end
def fields
@fields ||= begin
fkeys=associations.map(&:foreign_key).map(&:to_s)
fields=klass.columns.reject{ |col| col.name.to_s=='id' || fkeys.include?(col.name.to_s)}
# fields = klass.columns.map(&:name) - ['id'] - associations.map(&:foreign_key)
fields.map do |f|
begin
"#{f.type.to_s.classify}Field".constantize.new(f)
rescue Exception => e
Field.new(f)
end
end
end
end
end
desc "export model classes to something in Elixir that Just Might Work(tm)"
namespace :ex do
task :export => :environment do
mod=ENV['MODULE']
output_dir=ENV['OUTPUT']
unless !mod.blank? && !output_dir.blank?
$stdout.puts "You must supply both a module to put the Elixir models in and an output directory for them to go in, e.g.\n\t rake ex:export MODULE='MyApp.Models' OUTPUT='/path/to/my/elixir/project'"
exit
end
not_loadable=[]
not_ar=[]
template=ERB.new(TEMPLATE)
requested_files = ENV['FILES'].to_s.split(",")
klasses=(requested_files.any? ? requested_files : Dir.glob("app/models/**/*")).
each do |cl|
subdir=File.dirname(cl.gsub(%r|app/models/|, ''))
cname= cl.gsub(%r|app/models/|, '').gsub(%r|/[a-z]|){|m| m}.gsub(".rb", '').classify
begin
klass=cname.constantize
if !ActiveRecord::Base.subclasses.include?(klass)
not_ar << [cl, klass.name]
next
end
klass.connection
model=ModelWrapper.new(mod, klass)
write_dir=File.join(output_dir, 'web/models', subdir)
filename=File.join(write_dir, File.basename(cl).gsub(/\.rb/, '.ex'))
FileUtils.mkdir_p(write_dir)
if File.exists?(filename) && ENV['OVERWRITE'].blank?
$stdout.puts "Not writing to file #{filename} because it already exists. The generated schema for this file is: \n#{template.result(binding)}"
else
$stdout.puts "writing to #{filename}"
File.open(filename, 'w'){ |f| f.write(template.result(binding)) } unless ENV['DRY_RUN']
end
# puts template.result binding
rescue Exception => e
puts "Error processing #{cname} from #{cl}: #{ e }" if ENV['DEBUG']
debugger if ENV['DEBUG']
not_loadable << [cl, cname]
next
end
end
if not_loadable.any?
$stdout.puts "The following classes were unable to be loaded based on the filename convention used in Rails:\n\t#{not_loadable.map{ |arr| %|#{arr.last} not found in #{arr.first}| }.join("\n\t")}"
end
if not_ar.any?
$stdout.puts "The following classes were not subclasses of ActiveRecord::Base, so could not be exported into Ecto schema:\n\t#{not_loadable.map{ |arr| %|#{arr.last} not found in #{arr.first}| }.join("\n\t")}"
end
end
end
desc "export model classes to something in Elixir that Just Might Work(tm)"
namespace :ex do
task :export => :environment do
mod=ENV['MODULE']
output_dir=ENV['OUTPUT']
unless !mod.blank? && !output_dir.blank?
$stdout.puts "You must supply both a module to put the Elixir models in and an output directory for them to go in, e.g.\n\t rake ex:export MODULE='MyApp.Models' OUTPUT='/path/to/my/elixir/project'"
exit
end
not_loadable=[]
not_ar=[]
template=ERB.new(TEMPLATE)
requested_files = ENV['FILES'].to_s.split(",")
klasses=(requested_files.any? ? requested_files : Dir.glob("app/models/**/*")).
each do |cl|
subdir=File.dirname(cl.gsub(%r|app/models/|, ''))
cname= cl.gsub(%r|app/models/|, '').gsub(%r|/[a-z]|){|m| m}.gsub(".rb", '').classify
begin
klass=cname.constantize
if !ActiveRecord::Base.subclasses.include?(klass)
not_ar << [cl, klass.name]
next
end
klass.connection
model=ModelWrapper.new(mod, klass)
write_dir=File.join(output_dir, 'web/models', subdir)
filename=File.join(write_dir, File.basename(cl).gsub(/\.rb/, '.ex'))
FileUtils.mkdir_p(write_dir)
if File.exists?(filename) && ENV['OVERWRITE'].blank?
$stdout.puts "Not writing to file #{filename} because it already exists. The generated schema for this file is: \n#{template.result(binding)}"
else
$stdout.puts "writing to #{filename}"
File.open(filename, 'w'){ |f| f.write(template.result(binding)) } unless ENV['DRY_RUN']
end
# puts template.result binding
rescue Exception => e
puts "Error processing #{cname} from #{cl}: #{ e }" if ENV['DEBUG']
debugger if ENV['DEBUG']
not_loadable << [cl, cname]
next
end
end
if not_loadable.any?
$stdout.puts "The following classes were unable to be loaded based on the filename convention used in Rails:\n\t#{not_loadable.map{ |arr| %|#{arr.last} not found in #{arr.first}| }.join("\n\t")}"
end
if not_ar.any?
$stdout.puts "The following classes were not subclasses of ActiveRecord::Base, so could not be exported into Ecto schema:\n\t#{not_loadable.map{ |arr| %|#{arr.last} not found in #{arr.first}| }.join("\n\t")}"
end
TEMPLATE= <<-END_TEMPLATE
defmodule <%= mod %>.<%= model.to_ecto_model_name %> do
@moduledoc """
Originally imported from <%=model.name %>
"""
use <%= mod %>.Web, :model
schema "<%= model.table_name %>" do
<% model.associations.reject(&:through?).each do |assoc| %> <%= assoc.to_declaration %>
<% end %>
<% model.fields.each do |field| %> <%= field.to_declaration %>
<% end %>
end
end
END_TEMPLATE
- This code really, really sucks
- (Seriously, thinking about writing a book on un-sucking it)
- Plenty of todo's
- gem it up, clean it up, use actual options parsing, make it a legit executable
- Doesn't handle polymorphic associations
- (don't use polymorphic associations in the first place)
- order dependencies in associations
- This will get you like, 80% of the way there. Maybe.