Permalink
Browse files

First working cut of schematic. XSD generation for all attributes and…

… nested attributes of a model.

No restrictions from validation reflection yet.
  • Loading branch information...
1 parent 1471754 commit 7d20f9711362756d0699919f8c0fae68322f2b5b Brent Wheeldon & Michael Schubert committed Apr 1, 2011
Showing with 328 additions and 3 deletions.
  1. +10 −0 Rakefile
  2. +13 −1 lib/schematic.rb
  3. +115 −0 lib/schematic/serializers/xsd.rb
  4. +9 −2 schematic.gemspec
  5. +172 −0 spec/schematic_serializers_xsd_spec.rb
  6. +9 −0 spec/spec_helper.rb
View
@@ -1,2 +1,12 @@
+require 'rake'
+require 'rspec/core'
+require 'rspec/core/rake_task'
+
require 'bundler'
Bundler::GemHelper.install_tasks
+
+task :default => :spec
+
+desc "Run all specs in spec directory (excluding plugin specs)"
+RSpec::Core::RakeTask.new(:spec)
+
View
@@ -1,3 +1,15 @@
module Schematic
- # Your code goes here...
+ class InvalidClass < Exception
+ def message
+ "This class does not include ActiveModel. You cannot generate an XSD from it."
+ end
+ end
end
+
+require "builder"
+
+require 'active_support/inflector/inflections'
+require 'active_support/inflections'
+require "schematic/serializers/xsd"
+
+ActiveRecord::Base.send(:extend, Schematic::Serializers::Xsd)
@@ -0,0 +1,115 @@
+module Schematic
+ module Serializers
+ module Xsd
+ class << self
+ def extended(klass)
+ raise InvalidClass unless klass.ancestors.include?(ActiveRecord::Base)
+ end
+ end
+
+ def to_xsd(options = {}, builder = nil)
+ if builder.nil?
+ output = ""
+ builder = Builder::XmlMarkup.new(:target => output)
+ builder.instruct!
+ builder.xs :schema, "xmlns:xs" => "http://www.w3.org/2001/XMLSchema" do |schema|
+ schema.xs :element, "name" => xsd_element_collection_name, "type" => xsd_type_collection_name
+ self.to_xsd(options, schema)
+ end
+ output
+ else
+ xsd_nested_attributes.each do |nested_attribute|
+ nested_attribute.klass.to_xsd(options, builder)
+ end
+ builder.xs :complexType, "name" => xsd_type_collection_name do |complex_type|
+ complex_type.xs :sequence do |sequence|
+ sequence.xs :element, "name" => xsd_element_name, "type" => xsd_type_name, "minOccurs" => "0", "maxOccurs" => "unbounded"
+ end
+ complex_type.xs :attribute, "name" => "type", "type" => "xs:string", "fixed" => "array"
+ end
+ builder.xs :complexType, "name" => xsd_type_name do |complex_type|
+ additional_methods = xsd_methods.merge(options[:methods] || {})
+ complex_type.xs :all do |all|
+ xsd_columns.each do |column|
+ next if additional_methods.keys.map(&:to_s).include?(column.name)
+
+ all.xs :element, "name" => column.name.dasherize, "minOccurs" => "0", "maxOccurs" => "1" do |field|
+ field.xs :complexType do |complex_type|
+ complex_type.xs :simpleContent do |simple_content|
+ simple_content.xs :extension, "base" => map_column_type_to_xsd_type(column) do |extension|
+ extension.xs :attribute, "name" => "type", "type" => "xs:string", "use" => "optional"
+ end
+ end
+ end
+ end
+ end
+ xsd_nested_attributes.each do |nested_attribute|
+ all.xs :element, "name" => "#{nested_attribute.name.to_s.dasherize}-attributes", "type" => nested_attribute.klass.xsd_type_collection_name, "minOccurs" => "0", "maxOccurs" => "1"
+ end
+ additional_methods.each do |method_name, values|
+ method_xsd_name = method_name.to_s.dasherize
+ if values.present?
+ all.xs :element, "name" => method_xsd_name, "minOccurs" => "0", "maxOccurs" => "1" do |element|
+ element.xs :complexType do |complex_type|
+ complex_type.xs :all do |nested_all|
+ values.each do |value|
+ nested_all.xs :element, "name" => value.to_s.dasherize, "minOccurs" => "0"
+ end
+ end
+ complex_type.xs :attribute, "name" => "type", "type" => "xs:string", "fixed" => "array", "use" => "optional"
+ end
+ end
+ else
+ all.xs :element, "name" => method_xsd_name, "minOccurs" => "0", "maxOccurs" => "1"
+ end
+ end
+ end
+ end
+ builder
+ end
+ end
+
+ def xsd_methods
+ {}
+ end
+
+ def xsd_nested_attributes
+ self.reflect_on_all_associations.select do |association|
+ self.instance_methods.include?("#{association.name}_attributes=".to_sym) && association.options[:polymorphic] != true
+ end
+ end
+
+ def xsd_columns
+ self.columns
+ end
+
+ def map_column_type_to_xsd_type(column)
+ {
+ :integer => "xs:integer",
+ :float => "xs:float",
+ :string => "xs:string",
+ :text => "xs:string",
+ :datetime => "xs:dateTime",
+ :date => "xs:date",
+ :boolean => "xs:boolean"
+ }[column.type]
+ end
+
+ def xsd_type_name
+ self.name
+ end
+
+ def xsd_type_collection_name
+ xsd_type_name.pluralize
+ end
+
+ def xsd_element_name
+ self.name.underscore.dasherize
+ end
+
+ def xsd_element_collection_name
+ xsd_element_name.pluralize
+ end
+ end
+ end
+end
View
@@ -9,11 +9,18 @@ Gem::Specification.new do |s|
s.authors = ["Case Commons, LLC"]
s.email = ["casecommons-dev@googlegroups.com"]
s.homepage = "https://github.com/Casecommons/schematic"
- s.summary = %q{TODO: Write a gem summary}
- s.description = %q{TODO: Write a gem description}
+ s.summary = %q{Automatic XSD generation from ActiveRecord models}
+ s.description = %q{Automatic XSD generation from ActiveRecord models}
s.rubyforge_project = "schematic"
+ s.add_dependency('activerecord', '>= 3.0.0')
+ s.add_dependency('builder')
+ s.add_development_dependency('rspec-rails', '>= 2.1')
+ s.add_development_dependency('with_model')
+ s.add_development_dependency('nokogiri')
+ s.add_development_dependency('sqlite3')
+
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -0,0 +1,172 @@
+require "spec_helper"
+
+describe Schematic::Serializers::Xsd do
+ before do
+ class EmptyModel < ActiveRecord::Base
+
+ def self.columns
+ []
+ end
+ end
+ end
+
+ describe ".extend" do
+ context "when the model inherits ActiveRecord::Base" do
+ subject { EmptyModel }
+
+ it "should allow the model to be extended" do
+ lambda {
+ subject.class_eval do
+ extend Schematic::Serializers::Xsd
+ end
+ }.should_not raise_error
+ end
+ end
+
+ context "when the model does not inherit ActiveRecord::Base" do
+ subject { Object }
+
+ it "should raise an exception" do
+ lambda {
+ subject.class_eval do
+ extend Schematic::Serializers::Xsd
+ end
+ }.should raise_error(Schematic::InvalidClass)
+ end
+ end
+ end
+
+ describe ".to_xsd" do
+ context "for an empty model with no attributes or validations" do
+ subject { EmptyModel.to_xsd }
+
+ it "should return an xsd for an array of the model" do
+ xsd = <<-XML
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="empty-models" type="EmptyModels"/>
+ <xs:complexType name="EmptyModels">
+ <xs:sequence>
+ <xs:element name="empty-model" type="EmptyModel" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="type" type="xs:string" fixed="array"/>
+ </xs:complexType>
+ <xs:complexType name="EmptyModel">
+ <xs:all>
+ </xs:all>
+ </xs:complexType>
+</xs:schema>
+ XML
+ subject.should == sanitize_xml(xsd)
+ end
+
+ end
+
+ context "for a model with attributes" do
+
+ subject { SomeModel.to_xsd }
+
+ context "for a any attribute" do
+ with_model :some_model do
+ table :id => false do |t|
+ t.float 'some_float'
+ end
+ end
+
+ it "should define the correct xsd element" do
+ xsd = generate_xsd_for_model(SomeModel) do
+ <<-XML
+ <xs:element name="some-float" minOccurs="0" maxOccurs="1">
+ <xs:complexType>
+ <xs:simpleContent>
+ <xs:extension base="xs:float">
+ <xs:attribute name="type" type="xs:string" use="optional"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ </xs:element>
+ XML
+ end
+
+ subject.should == sanitize_xml(xsd)
+ end
+
+ end
+
+ describe "additional methods" do
+ with_model :some_model do
+ table {}
+ end
+
+ it "should include the additional method" do
+ xsd = generate_xsd_for_model(SomeModel) do
+ <<-XML
+ <xs:element name="id" minOccurs="0" maxOccurs="1">
+ <xs:complexType>
+ <xs:simpleContent>
+ <xs:extension base="xs:integer">
+ <xs:attribute name="type" type="xs:string" use="optional"/>
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ </xs:element>
+ <xs:element name="foo-bar" minOccurs="0" maxOccurs="1"/>
+ XML
+ end
+
+ SomeModel.to_xsd(:methods => {:foo_bar => nil}).should == sanitize_xml(xsd)
+ end
+ end
+
+ describe "nested attributes" do
+
+ end
+
+ end
+
+ context "with a model with validations" do
+ context "presence of validation" do
+ context "when allow blank is true" do
+
+ end
+
+ context "when allow blank is false" do
+ end
+ end
+
+ describe "length validation" do
+
+ end
+
+ describe "inclusion validation" do
+
+ end
+ end
+ end
+
+ private
+
+ def sanitize_xml(xml)
+ xml.split("\n").map(&:strip).join("")
+ end
+
+ def generate_xsd_for_model(model)
+ <<-XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="#{model.xsd_element_collection_name}" type="#{model.xsd_type_collection_name}"/>
+ <xs:complexType name="#{model.xsd_type_collection_name}">
+ <xs:sequence>
+ <xs:element name="#{model.xsd_element_name}" type="#{model.xsd_type_name}" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="type" type="xs:string" fixed="array"/>
+ </xs:complexType>
+ <xs:complexType name="#{model.xsd_type_name}">
+ <xs:all>
+ #{yield}
+ </xs:all>
+ </xs:complexType>
+ </xs:schema>
+ XML
+ end
+end
View
@@ -0,0 +1,9 @@
+require "active_record"
+require "with_model"
+require "schematic"
+
+RSpec.configure do |config|
+ config.extend WithModel
+end
+
+ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")

0 comments on commit 7d20f97

Please sign in to comment.