-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 32fdcec
Showing
10 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/.bundle/ | ||
/.yardoc | ||
/_yardoc/ | ||
/coverage/ | ||
/doc/ | ||
/pkg/ | ||
/spec/reports/ | ||
/tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
source "https://rubygems.org" | ||
|
||
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } | ||
|
||
# Specify your gem's dependencies in dbml.gemspec | ||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2020 Simon Worthington | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# DBML | ||
|
||
Ruby library for [Database Markup Language (DBML)](https://www.dbml.org/). | ||
|
||
## Installation | ||
|
||
Add this line to your application's Gemfile: | ||
|
||
```ruby | ||
gem 'dbml' | ||
``` | ||
|
||
And then execute: | ||
|
||
$ bundle | ||
|
||
Or install it yourself as: | ||
|
||
$ gem install dbml | ||
|
||
## Usage | ||
|
||
```ruby | ||
require 'dbml' | ||
project = DBML::Parser.parse """ | ||
Table users { | ||
id int64 [pk, unique] | ||
name varchar [unique] | ||
Note: 'add some more things in here!' | ||
}""" | ||
puts project.tables.first.name | ||
# => "users" | ||
``` | ||
|
||
## Development | ||
|
||
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests. | ||
|
||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). | ||
|
||
## Contributing | ||
|
||
Bug reports and pull requests are welcome on GitHub at https://github.com/simonwo/dbml-ruby. | ||
|
||
## License | ||
|
||
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
require "bundler/gem_tasks" | ||
require "rake/testtask" | ||
|
||
Rake::TestTask.new(:test) do |t| | ||
t.libs << "test" | ||
t.libs << "lib" | ||
t.test_files = FileList["test/**/*_test.rb"] | ||
end | ||
|
||
task :default => :test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
|
||
lib = File.expand_path('../lib', __FILE__) | ||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | ||
require 'dbml/version' | ||
|
||
Gem::Specification.new do |spec| | ||
spec.name = 'dbml' | ||
spec.version = DBML::VERSION | ||
spec.authors = ['Simon Worthington'] | ||
spec.email = ['[email protected]'] | ||
|
||
spec.summary = %q{Parser for Database Markup Language (DBML).} | ||
spec.homepage = 'https://github.com/simonwo/dbml-ruby' | ||
spec.license = 'MIT' | ||
|
||
# Specify which files should be added to the gem when it is released. | ||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git. | ||
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do | ||
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } | ||
end | ||
spec.bindir = 'exe' | ||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } | ||
spec.require_paths = ['lib'] | ||
|
||
spec.add_dependency 'rsec', '~> 1.0' | ||
spec.add_development_dependency 'bundler' | ||
spec.add_development_dependency 'rake' | ||
spec.add_development_dependency 'minitest', '~> 5.0' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
require 'rsec' | ||
include Rsec::Helpers | ||
|
||
module DBML | ||
Column = Struct.new :name, :type, :settings | ||
Table = Struct.new :name, :alias, :notes, :columns, :indexes | ||
Index = Struct.new :fields, :settings | ||
Expression = Struct.new :text | ||
Enum = Struct.new :name, :choices | ||
EnumChoice = Struct.new :name, :settings | ||
TableGroup = Struct.new :name, :tables | ||
Project = Struct.new :name, :notes, :settings, :tables, :enums, :table_groups | ||
ProjectDef = Struct.new :name, :notes, :settings | ||
|
||
module Parser | ||
def self.long_or_short p | ||
(':'.r >> p) | ('{'.r >> p << '}'.r) | ||
end | ||
|
||
def self.unwrap p, *_ | ||
if p.empty? then nil else p.first end | ||
end | ||
|
||
def self.comma_separated p | ||
p.join(/, */.r.map {|_| nil}).star.map {|v| v.first.reject(&:nil?) } | ||
end | ||
|
||
def self.space_surrounded p | ||
/\s*/.r >> p << /\s*/.r | ||
end | ||
|
||
def self.block type, name_parser, content_parser, &block | ||
seq_(type.r >> name_parser, '{'.r >> space_surrounded(content_parser).star.map {|a| a.flatten(1) } << '}'.r, &block) | ||
end | ||
|
||
BOOLEAN = 'true'.r | 'false'.r | ||
NULL = 'null'.r | ||
NUMBER = prim(:double) | ||
EXPRESSION = seq('`'.r, /[^`]+/.r, '`'.r)[1].map {|str| Expression.new str} | ||
SINGLE_LING_STRING = seq("'".r, /[^']+/.r, "'".r)[1] | ||
MULTI_LINE_STRING = seq("'''".r, /([^']|'[^']|''[^'])+/m.r, "'''".r)[1].map do |string| | ||
# Remove the indentation on the first line from all other lines. | ||
indent = string.match(/^\s*/m)[0].size | ||
string.lines.map do |line| | ||
raise "Indentation does not match" unless line =~ /\s{#{indent}}/ | ||
line[indent..] | ||
end.join | ||
end | ||
STRING = SINGLE_LING_STRING | MULTI_LINE_STRING | ||
ATOM = BOOLEAN | NULL | NUMBER | EXPRESSION | STRING | ||
|
||
# Each setting item can take in 2 forms: Key: Value or keyword, similar to that of Python function parameters. | ||
SETTING = seq_(/[^,:\[\]\{\}\s]+/.r, (':'.r >> ATOM).maybe(&method(:unwrap))) {|(key, value)| {key => value} } | ||
# Settings are all defined within square brackets: [setting1: value1, setting2: value2, setting3, setting4] | ||
SETTINGS = ('['.r >> comma_separated(SETTING) << ']'.r).map {|values| values.reduce({}, &:update) } | ||
|
||
NOTE = 'Note'.r >> (long_or_short STRING) | ||
|
||
# Index Definition | ||
# | ||
# Indexes allow users to quickly locate and access the data. Users can define single or multi-column indexes. | ||
# | ||
# Table bookings { | ||
# id integer | ||
# country varchar | ||
# booking_date date | ||
# created_at timestamp | ||
# | ||
# indexes { | ||
# (id, country) [pk] // composite primary key | ||
# created_at [note: 'Date'] | ||
# booking_date | ||
# (country, booking_date) [unique] | ||
# booking_date [type: hash] | ||
# (`id*2`) | ||
# (`id*3`,`getdate()`) | ||
# (`id*3`,id) | ||
# } | ||
# } | ||
# | ||
# There are 3 types of index definitions: | ||
# | ||
# Index with single field (with index name): CREATE INDEX on users (created_at) | ||
# Index with multiple fields (composite index): CREATE INDEX on users (created_at, country) | ||
# Index with an expression: CREATE INDEX ON films ( first_name + last_name ) | ||
# (bonus) Composite index with expression: CREATE INDEX ON users ( country, (lower(name)) ) | ||
|
||
INDEX_SINGLE = /[^\(\)\,\{\}\s]+/.r | ||
INDEX_COMPOSITE = seq_('('.r, comma_separated(EXPRESSION | INDEX_SINGLE), ')'.r).inner.map {|v| unwrap(v) } | ||
INDEX = seq_(INDEX_SINGLE | INDEX_COMPOSITE, SETTINGS.maybe).map do |(fields, settings)| | ||
Index.new fields, unwrap(settings) | ||
end | ||
INDEXES = ('indexes {'.r >> INDEX.star << '}'.r).map{|v| p v; v } | ||
|
||
# Enum Definition | ||
# --------------- | ||
# | ||
# Enum allows users to define different values of a particular column. | ||
# | ||
# enum job_status { | ||
# created [note: 'Waiting to be processed'] | ||
# running | ||
# done | ||
# failure | ||
# } | ||
|
||
ENUM_CHOICE = seq_(/\S+/.r, SETTINGS.maybe).map {|(name, settings)| EnumChoice.new name, unwrap(settings) } | ||
ENUM = seq_('enum'.r >> /\S+/.r, '{'.r >> ENUM_CHOICE.star << '}'.r).map {|(name, choices)| Enum.new name, choices } | ||
|
||
# Column Definition | ||
# ================= | ||
# * name of the column is listed as column_name | ||
# * type of the data in the column listed as column_type | ||
# * supports all data types, as long as it is a single word (remove all spaces in the data type). Example, JSON, JSONB, decimal(1,2), etc. | ||
# * column_name can be stated in just plain text, or wrapped in a double quote as "column name" | ||
# | ||
# Column Settings | ||
# --------------- | ||
# Each column can take have optional settings, defined in square brackets like: | ||
# | ||
# Table buildings { | ||
# ... | ||
# address varchar(255) [unique, not null, note: 'to include unit number'] | ||
# id integer [ pk, unique, default: 123, note: 'Number' ] | ||
# } | ||
|
||
QUOTED_COLUMN_NAME = '"'.r >> /[^"]+/.r << '"'.r | ||
UNQUOTED_COLUMN_NAME = /\S+/.r | ||
COLUMN_TYPE = /\S+/.r | ||
COLUMN = seq_( | ||
QUOTED_COLUMN_NAME | UNQUOTED_COLUMN_NAME, | ||
COLUMN_TYPE, | ||
SETTINGS.maybe | ||
) {|(name, type, settings)| Column.new name, type, unwrap(settings) } | ||
|
||
# Table Definition | ||
# | ||
# Table table_name { | ||
# column_name column_type [column_settings] | ||
# } | ||
# | ||
# * title of database table is listed as table_name | ||
# * list is wrapped in curly brackets {}, for indexes, constraints and table definitions. | ||
# * string value is be wrapped in a single quote as 'string' | ||
|
||
TABLE_NAME = seq_ /\S+/.r, ('as'.r >> /\S+/.r).maybe(&method(:unwrap)) | ||
TABLE = seq_('Table'.r >> TABLE_NAME, (long_or_short (/\s*/.r >> (INDEXES | NOTE | COLUMN) << /\s*/.r).star)) do |((name, aliaz), objects)| | ||
Table.new name, aliaz, | ||
objects.select {|o| o.is_a? String }, | ||
objects.select {|o| o.is_a? Column }, | ||
objects.select {|o| (o.is_a? Array) && (o.all? {|e| e.is_a? Index })}.flatten | ||
end | ||
|
||
# TableGroup | ||
# ========== | ||
# | ||
# TableGroup allows users to group the related or associated tables together. | ||
# | ||
# TableGroup tablegroup_name { // tablegroup is case-insensitive. | ||
# table1 | ||
# table2 | ||
# table3 | ||
# } | ||
TABLE_GROUP = seq_('TableGroup'.r >> /\S+/.r, '{'.r >> /\S+/.r.star << '}'.r) do |(name, tables)| | ||
TableGroup.new name, tables | ||
end | ||
|
||
# Project Definition | ||
# ================== | ||
# You can give overall description of the project. | ||
# | ||
# Project project_name { | ||
# database_type: 'PostgreSQL' | ||
# Note: 'Description of the project' | ||
# } | ||
|
||
PROJECT_DEFINITION = block 'Project', /\S+/.r, (NOTE | SETTING).star do |(name, objects)| | ||
ProjectDef.new name, | ||
objects.select {|o| o.is_a? String }, | ||
objects.select {|o| o.is_a? Hash }.reduce({}, &:update) | ||
end | ||
PROJECT = space_surrounded(PROJECT_DEFINITION | TABLE | TABLE_GROUP | ENUM).star do |objects| | ||
definition = objects.find {|o| o.is_a? ProjectDef } | ||
Project.new definition.nil? ? nil : definition.name, | ||
definition.nil? ? [] : definition.notes, | ||
definition.nil? ? [] : definition.settings, | ||
objects.select {|o| o.is_a? Table }, | ||
objects.select {|o| o.is_a? Enum }, | ||
objects.select {|o| o.is_a? TableGroup } | ||
end | ||
|
||
def self.parse str | ||
PROJECT.eof.parse! str.gsub(/\/{2}.*$/, '') | ||
end | ||
end | ||
end | ||
|
||
if $0 == __FILE__ | ||
p DBML::Parser::PROJECT_DEFINITION.parse! "Project geoff {\n database_table: 'PostgreSQL'\n}" | ||
p DBML::Parser::NOTE.parse! "Note { \n''' Simon is\n very wicked'''\n}" | ||
p DBML::Parser::SETTINGS.parse! "[long: 'short', unique, not null, default: 123.45678]" | ||
p DBML::Parser::INDEX.eof.parse! '(id, `id*3`) [pk]' | ||
x = DBML::Parser.parse "Project geoff {\n database_type: 'postgres'\n}\nTable banter {\n id string [pk]// the id \n// TODO: rest of schema\nNote: 'this is a great table'\nindexes { id\n(id, `id*2`)\n} }" | ||
p x.tables | ||
p x.name | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module DBML | ||
VERSION = "0.1.0" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
require "test_helper" | ||
|
||
class DbmlTest < Minitest::Test | ||
def test_that_it_has_a_version_number | ||
refute_nil ::Dbml::VERSION | ||
end | ||
|
||
def test_it_does_something_useful | ||
assert false | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) | ||
require "dbml" | ||
|
||
require "minitest/autorun" |