Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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...
commit 7d20f9711362756d0699919f8c0fae68322f2b5b 1 parent 1471754
Brent Wheeldon & Michael Schubert authored
View
10 Rakefile
@@ -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
14 lib/schematic.rb
@@ -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)
View
115 lib/schematic/serializers/xsd.rb
@@ -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
11 schematic.gemspec
@@ -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) }
View
172 spec/schematic_serializers_xsd_spec.rb
@@ -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
9 spec/spec_helper.rb
@@ -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:")
Please sign in to comment.
Something went wrong with that request. Please try again.