Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

screw svn, initial commit

  • Loading branch information...
commit 2181f196876b6d0a5f24623c72c90e0c2586b205 0 parents
Philipp Hofmann authored
7 META.yml
@@ -0,0 +1,7 @@
+author: phil hofmann
+summary: generate model and their associations from uml
+homepage: http://branch14.org/ruby_on_rails/plugins/generate_from_uml/
+plugin: http://dev.branch14.org/svn/rails_plugins/generate_from_uml/
+license: MIT
+version: 0.1
+rails_version: 1.1.2+
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
75 README
@@ -0,0 +1,75 @@
+GenerateFromUML Plugin
+======================
+
+GenerateFromUML is a RubyOnRails Plugin that generates models (and
+their migrations, unit tests and fixtures) as well as their
+associations' statements (belongs_to, has_many, ...) from an UML Class
+Diagram.
+
+Supported Features
+==================
+
+Supported are the DIA internal format, as well as XMI.
+
+UMLwise it's currently pretty basic. Nothing fancy so far.
+
+ classes
+ properties
+ associations
+ 1 - *
+ 0..1 - *
+ * - * (doesn't generate join tables)
+
+An Example
+==========
+
+Lets suppose you have an UML Class Diagram that looks similar to the
+following and is stored in a file named 'diagram.dia'.
+
+ ----------------- 1 * -------------------
+ | Customer | --------------- | Order |
+ |---------------| |-----------------|
+ | +name: string | | +number: string |
+ |---------------| |-----------------|
+ ----------------- -------------------
+
+By calling the rake task 'uml:generate' like this:
+
+ $ rake uml:generate filename=diagram.dia
+
+the two models Customer and Order (and their migrations, unit tests
+and fixtures) will be generated.
+
+ $ cat app/models/order.rb
+ class Order < ActiveRecord::Base
+ belongs_to :customer
+ end
+
+ $ cat app/models/customer.rb
+ class Customer < ActiveRecord::Base
+ has_many :orders
+ end
+
+ $ cat db/migrate/001_create_orders.rb
+ class CreateCustomers < ActiveRecord::Migration
+ def self.up
+ create_table :orders do |t|
+ t.string :number
+ t.integer :customer_id
+ t.timestamps
+ end
+ end
+ def self.down
+ drop_table :orders
+ end
+ end
+
+I guess, you got it. Have fun.
+
+FOR THE UNPATIENT WHO READ THE BOTTOM FIRST
+===========================================
+
+ $ rake uml:generate filename=diagram.dia
+
+--
+Copyright (c) 2008 Phil Hofmann, released under the MIT license
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the uml2rails plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the uml2rails plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'Uml2rails'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
0  generators/umlified_model/USAGE
No changes.
19 generators/umlified_model/templates/fixtures.yml
@@ -0,0 +1,19 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+<% unless attributes.empty? -%>
+one:
+<% for attribute in attributes -%>
+ <%= attribute.name %>: <%= attribute.default %>
+<% end -%>
+
+two:
+<% for attribute in attributes -%>
+ <%= attribute.name %>: <%= attribute.default %>
+<% end -%>
+<% else -%>
+# one:
+# column: value
+#
+# two:
+# column: value
+<% end -%>
16 generators/umlified_model/templates/migration.rb
@@ -0,0 +1,16 @@
+class <%= migration_name %> < ActiveRecord::Migration
+ def self.up
+ create_table :<%= table_name %> do |t|
+<% for attribute in attributes -%>
+ t.<%= attribute.type %> :<%= attribute.name %>
+<% end -%>
+<% unless options[:skip_timestamps] %>
+ t.timestamps
+<% end -%>
+ end
+ end
+
+ def self.down
+ drop_table :<%= table_name %>
+ end
+end
5 generators/umlified_model/templates/model.rb
@@ -0,0 +1,5 @@
+class <%= class_name %> < ActiveRecord::Base
+
+<%= associations.join("\n") %>
+
+end
8 generators/umlified_model/templates/unit_test.rb
@@ -0,0 +1,8 @@
+require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
+
+class <%= class_name %>Test < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
7 generators/umlified_model/umlified_model_generator.rb
@@ -0,0 +1,7 @@
+class UmlifiedModelGenerator < Rails::Generator::ModelGenerator
+
+ def associations
+ options[:associations]
+ end
+
+end
1  install.rb
@@ -0,0 +1 @@
+puts File.read(File.join(File.dirname(__FILE__), 'README'))
191 lib/#generate_from_uml.rb#
@@ -0,0 +1,191 @@
+
+require 'zlib'
+require 'rexml/document'
+
+require 'logger'
+
+LOG = Logger.new(STDOUT)
+
+# require 'active_support/core_ext/string/inflections'
+# String.send(:include, ActiveSupport::CoreExtensions::String::Inflections)
+
+module UML
+
+ class Association < Struct.new(:klass, :multiplicities, :through); end
+
+ class Attribute < Struct.new(:name, :type); end
+
+ class Klass
+
+ attr_reader :oid, :name
+
+ def initialize(oid, name)
+ @oid, @name, @aa = oid, name, []
+ end
+
+ def <<(thing)
+ @aa << thing
+ end
+
+ def associations
+ @aa.select { |a| a.is_a?(Association) }
+ end
+
+ def attributes
+ @aa.select { |a| a.is_a?(Attribute) }
+ end
+
+ end
+
+ class Design
+
+ attr_reader :klasses # debugging
+
+ def initialize
+ @klasses = {}
+ end
+
+ def <<(klass)
+ @klasses[klass.oid] = klass
+ end
+
+ def associate(oid1, oid2, m1, m2)
+ @klasses[oid1] << Association.new(@klasses[oid2], "#{m1} -> #{m2}")
+ @klasses[oid2] << Association.new(@klasses[oid1], "#{m2} -> #{m1}")
+ end
+
+ def emit
+ @klasses.each do |oid, klass|
+ options = { :associations => [] }
+ attributes = klass.attributes.map { |a| "#{a.name}:#{a.type}" }
+ klass.associations.each do |assoc|
+ case assoc.multiplicities
+ when '* -> 1', '* -> 0..1'
+ options[:associations] << "belongs_to :#{assoc.klass.name.underscore}"
+ attributes << "#{assoc.klass.name.underscore}_id:integer"
+ when '1 -> *', '0..1 -> *'
+ options[:associations] << "has_many :#{assoc.klass.name.underscore.pluralize}"
+ #when '* -> *'
+ # options[:associations] << assoc.through ?
+ # "has_many :#{assoc.klass.name.underscore.pluralize}, :through => #{assoc.through.underscore.pluralize}" :
+ # "has_and_belongs_to_many :#{assoc.klass.name.underscore.pluralize}"
+ end
+ end
+ args = ['umlified_model', klass.name] + attributes
+ LOG.debug('-'*60+"\nrunning generator ...\n\targs: #{args.join(' ')}\n\toptions: #{options}\n"+'-'*60)
+ Rails::Generator::Scripts::Generate.new.run(args, options)
+ end
+ end
+
+ def self.load(options)
+ # for now it only supports dia and the internal yaml format
+ ext = File.extname(options[:filename])
+ case ext
+ when '.yml' then load_yml(options)
+ when '.dia' then load_dia(options)
+ when '.xmi' then load_xmi(options)
+ else
+ raise "This format '#{ext}' is not supported"
+ end
+ end
+
+ def self.load_yml(options)
+ YAML.load(File.read(options[:filename]))
+ end
+
+ def self.load_dia(options)
+ LOG.debug("loading dia with options: #{options}")
+ # dia is gzip'd xml so we'll load it using zlib and rexml
+ # TODO fallback to ungzip'd attempt of loading the xml
+ root = Zlib::GzipReader.open(options[:filename]) { |gz| REXML::Document.new(gz.read).root }
+ design = Design.new
+ # go through the classes of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Class"]'
+ REXML::XPath.each(root, xpath) do |xml_klass|
+ oid = xml_klass.attribute(:id).value
+ name = dia_string(xml_klass, 'name')
+ design << uml_klass = Klass.new(oid, name)
+ # go through the attributes of the class
+ xpath = 'child::dia:attribute[@name="attributes"]/dia:composite'
+ REXML::XPath.each(xml_klass, xpath) do |attrib|
+ name = dia_string(attrib, 'name')
+ type = dia_string(attrib, 'type')
+ name, type = name.underscore, type.downcase if options[:force_conventions]
+ uml_klass << Attribute.new(name, type)
+ end
+ end
+ # go through the associations of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Association"]'
+ REXML::XPath.each(root, xpath) do |assoc|
+ direction = REXML::XPath.first(assoc, 'child::dia:attribute[@name="direction"]/dia:enum').text.to_i
+ multiplicities = []
+ xpath = 'child::dia:attribute[@name="ends"]/dia:composite'
+ REXML::XPath.each(assoc, xpath) do |ent|
+ multiplicities << dia_string(ent, 'multiplicity')
+ end
+ connections = []
+ xpath = 'child::dia:connections/dia:connection'
+ REXML::XPath.each(assoc, xpath) do |conn|
+ to = conn.attribute(:to).value
+ handle = conn.attribute(:handle).value.to_i
+ connections[handle ^ direction] = to
+ end
+ # design.associate(connections[0], connections[1], multiplicities[0], multiplicities[1])
+ design.associate(*(connections + multiplicities))
+ end
+ return design
+ end
+
+ def self.load_xmi(options)
+ LOG.debug("loading xmi with options: #{options}")
+ # dia is gzip'd xml so we'll load it using zlib and rexml
+ # TODO fallback to ungzip'd attempt of loading the xml
+ root = Zlib::GzipReader.open(options[:filename]) { |gz| REXML::Document.new(gz.read).root }
+ design = Design.new
+ # go through the classes of all visible layers
+ xpath = '/XMI/XMI.content/UML:Model/UML:Namespace.ownedElement/UML:Class'
+ REXML::XPath.each(root, xpath) do |xml_klass|
+ oid = xml_klass.attribute('xmi.id').value
+ name = xml_klass.attribute('name').value
+ design << uml_klass = Klass.new(oid, name)
+ # go through the attributes of the class
+ ### AB HIER !!! ###
+ xpath = 'child::dia:attribute[@name="attributes"]/dia:composite'
+ REXML::XPath.each(xml_klass, xpath) do |attrib|
+ name = dia_string(attrib, 'name')
+ type = dia_string(attrib, 'type')
+ name, type = name.underscore, type.downcase if options[:force_conventions]
+ uml_klass << Attribute.new(name, type)
+ end
+ end
+ # go through the associations of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Association"]'
+ REXML::XPath.each(root, xpath) do |assoc|
+ direction = REXML::XPath.first(assoc, 'child::dia:attribute[@name="direction"]/dia:enum').text.to_i
+ multiplicities = []
+ xpath = 'child::dia:attribute[@name="ends"]/dia:composite'
+ REXML::XPath.each(assoc, xpath) do |ent|
+ multiplicities << dia_string(ent, 'multiplicity')
+ end
+ connections = []
+ xpath = 'child::dia:connections/dia:connection'
+ REXML::XPath.each(assoc, xpath) do |conn|
+ to = conn.attribute(:to).value
+ handle = conn.attribute(:handle).value.to_i
+ connections[handle ^ direction] = to
+ end
+ # design.associate(connections[0], connections[1], multiplicities[0], multiplicities[1])
+ design.associate(*(connections + multiplicities))
+ end
+ return design
+ end
+
+ private
+
+ def self.dia_string(xml, name)
+ REXML::XPath.first(xml, "child::dia:attribute[@name='#{name}']/dia:string").text.sub(/#(.*)#/, '\1')
+ end
+
+ end
+
+end
191 lib/generate_from_uml.rb
@@ -0,0 +1,191 @@
+
+require 'zlib'
+require 'rexml/document'
+
+require 'logger'
+
+LOG = Logger.new(STDOUT)
+
+# require 'active_support/core_ext/string/inflections'
+# String.send(:include, ActiveSupport::CoreExtensions::String::Inflections)
+
+module UML
+
+ class Association < Struct.new(:klass, :multiplicities, :through); end
+
+ class Attribute < Struct.new(:name, :type); end
+
+ class Klass
+
+ attr_reader :oid, :name
+
+ def initialize(oid, name)
+ @oid, @name, @aa = oid, name, []
+ end
+
+ def <<(thing)
+ @aa << thing
+ end
+
+ def associations
+ @aa.select { |a| a.is_a?(Association) }
+ end
+
+ def attributes
+ @aa.select { |a| a.is_a?(Attribute) }
+ end
+
+ end
+
+ class Design
+
+ attr_reader :klasses # debugging
+
+ def initialize
+ @klasses = {}
+ end
+
+ def <<(klass)
+ @klasses[klass.oid] = klass
+ end
+
+ def associate(oid1, oid2, m1, m2)
+ @klasses[oid1] << Association.new(@klasses[oid2], "#{m1} -> #{m2}")
+ @klasses[oid2] << Association.new(@klasses[oid1], "#{m2} -> #{m1}")
+ end
+
+ def emit
+ @klasses.each do |oid, klass|
+ options = { :associations => [] }
+ attributes = klass.attributes.map { |a| "#{a.name}:#{a.type}" }
+ klass.associations.each do |assoc|
+ case assoc.multiplicities
+ when '* -> 1', '* -> 0..1'
+ options[:associations] << "belongs_to :#{assoc.klass.name.underscore}"
+ attributes << "#{assoc.klass.name.underscore}_id:integer"
+ when '1 -> *', '0..1 -> *'
+ options[:associations] << "has_many :#{assoc.klass.name.underscore.pluralize}"
+ #when '* -> *'
+ # options[:associations] << assoc.through ?
+ # "has_many :#{assoc.klass.name.underscore.pluralize}, :through => #{assoc.through.underscore.pluralize}" :
+ # "has_and_belongs_to_many :#{assoc.klass.name.underscore.pluralize}"
+ end
+ end
+ args = ['umlified_model', klass.name] + attributes
+ LOG.debug('-'*60+"\nrunning generator ...\n\targs: #{args.join(' ')}\n\toptions: #{options}\n"+'-'*60)
+ Rails::Generator::Scripts::Generate.new.run(args, options)
+ end
+ end
+
+ def self.load(options)
+ # for now it only supports dia and the internal yaml format
+ ext = File.extname(options[:filename])
+ case ext
+ when '.yml' then load_yml(options)
+ when '.dia' then load_dia(options)
+ when '.xmi' then load_xmi(options)
+ else
+ raise "This format '#{ext}' is not supported"
+ end
+ end
+
+ def self.load_yml(options)
+ YAML.load(File.read(options[:filename]))
+ end
+
+ def self.load_dia(options)
+ LOG.debug("loading dia with options: #{options}")
+ # dia is gzip'd xml so we'll load it using zlib and rexml
+ # TODO fallback to ungzip'd attempt of loading the xml
+ root = Zlib::GzipReader.open(options[:filename]) { |gz| REXML::Document.new(gz.read).root }
+ design = Design.new
+ # go through the classes of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Class"]'
+ REXML::XPath.each(root, xpath) do |xml_klass|
+ oid = xml_klass.attribute(:id).value
+ name = dia_string(xml_klass, 'name')
+ design << uml_klass = Klass.new(oid, name)
+ # go through the attributes of the class
+ xpath = 'child::dia:attribute[@name="attributes"]/dia:composite'
+ REXML::XPath.each(xml_klass, xpath) do |attrib|
+ name = dia_string(attrib, 'name')
+ type = dia_string(attrib, 'type')
+ name, type = name.underscore, type.downcase if options[:force_conventions]
+ uml_klass << Attribute.new(name, type)
+ end
+ end
+ # go through the associations of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Association"]'
+ REXML::XPath.each(root, xpath) do |assoc|
+ direction = REXML::XPath.first(assoc, 'child::dia:attribute[@name="direction"]/dia:enum').text.to_i
+ multiplicities = []
+ xpath = 'child::dia:attribute[@name="ends"]/dia:composite'
+ REXML::XPath.each(assoc, xpath) do |ent|
+ multiplicities << dia_string(ent, 'multiplicity')
+ end
+ connections = []
+ xpath = 'child::dia:connections/dia:connection'
+ REXML::XPath.each(assoc, xpath) do |conn|
+ to = conn.attribute(:to).value
+ handle = conn.attribute(:handle).value.to_i
+ connections[handle ^ direction] = to
+ end
+ # design.associate(connections[0], connections[1], multiplicities[0], multiplicities[1])
+ design.associate(*(connections + multiplicities))
+ end
+ return design
+ end
+
+ def self.load_xmi(options)
+ LOG.debug("loading dia with options: #{options}")
+ # dia is gzip'd xml so we'll load it using zlib and rexml
+ # TODO fallback to ungzip'd attempt of loading the xml
+ root = Zlib::GzipReader.open(options[:filename]) { |gz| REXML::Document.new(gz.read).root }
+ design = Design.new
+ # go through the classes of all visible layers
+ xpath = '/XMI/XMI.content/UML:Model/UML:Namespace.ownedElement/UML:Class'
+ REXML::XPath.each(root, xpath) do |xml_klass|
+ oid = xml_klass.attribute('xmi.id').value
+ name = xml_klass.attribute('name').value
+ design << uml_klass = Klass.new(oid, name)
+ # go through the attributes of the class
+ ### AB HIER !!! ###
+ xpath = 'child::dia:attribute[@name="attributes"]/dia:composite'
+ REXML::XPath.each(xml_klass, xpath) do |attrib|
+ name = dia_string(attrib, 'name')
+ type = dia_string(attrib, 'type')
+ name, type = name.underscore, type.downcase if options[:force_conventions]
+ uml_klass << Attribute.new(name, type)
+ end
+ end
+ # go through the associations of all visible layers
+ xpath = '/dia:diagram/dia:layer[@visible="true"]/dia:object[@type="UML - Association"]'
+ REXML::XPath.each(root, xpath) do |assoc|
+ direction = REXML::XPath.first(assoc, 'child::dia:attribute[@name="direction"]/dia:enum').text.to_i
+ multiplicities = []
+ xpath = 'child::dia:attribute[@name="ends"]/dia:composite'
+ REXML::XPath.each(assoc, xpath) do |ent|
+ multiplicities << dia_string(ent, 'multiplicity')
+ end
+ connections = []
+ xpath = 'child::dia:connections/dia:connection'
+ REXML::XPath.each(assoc, xpath) do |conn|
+ to = conn.attribute(:to).value
+ handle = conn.attribute(:handle).value.to_i
+ connections[handle ^ direction] = to
+ end
+ # design.associate(connections[0], connections[1], multiplicities[0], multiplicities[1])
+ design.associate(*(connections + multiplicities))
+ end
+ return design
+ end
+
+ private
+
+ def self.dia_string(xml, name)
+ REXML::XPath.first(xml, "child::dia:attribute[@name='#{name}']/dia:string").text.sub(/#(.*)#/, '\1')
+ end
+
+ end
+
+end
46 tasks/generate_from_uml_tasks.rake
@@ -0,0 +1,46 @@
+def dry_setup
+ require File.dirname(__FILE__) + '/../../../../config/boot'
+ require "#{RAILS_ROOT}/config/environment"
+ require 'rails_generator'
+ require 'rails_generator/scripts/generate'
+ require File.join(File.dirname(__FILE__), "../lib/generate_from_uml")
+ options = {
+ :filename => ENV['filename'],
+ :force_conventions => ENV['force_conventions'] | true # FIXME
+ }
+end
+
+namespace :uml do
+
+ desc "Generate models and associations from uml class diagramm (DIA) or YAML"
+ task :generate do
+ raise "usage: rake uml:generate filename=<path_to_file>" unless ENV.include?("filename")
+ options = dry_setup
+ UML::Design.load(options).emit
+ end
+
+ desc "Convert a Rails app design in a DIA diagram to yaml"
+ task :yaml do
+ raise "usage: rake uml:yaml filename=<path_to_file>" unless ENV.include?("filename")
+ options = dry_setup
+ puts UML::Design.load(options).to_yaml
+ end
+
+ desc "Purge all in app/models, test/unit, test/fixtures, db/migrate"
+ task :purge_ALL do
+ begin
+ require 'highline'
+ highline = HighLine.new
+ return unless highline.agree('> Please think twice. Really purge everything?')
+ highline.say('> *sigh*')
+ ['app/models', 'test/unit', 'test/fixtures', 'db/migrate'].each do |path|
+ puts "purging all in #{path}"
+ FileUtils.rm Dir.glob("#{path}/*")
+ end
+ highline.say('> I hope you had a good reason to do that!')
+ rescue
+ puts "> Sorry, I won't let you use this task if haven't even the highline gem installed."
+ end
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.