Skip to content

Commit

Permalink
Parse setting keys and unquoted values as symbols
Browse files Browse the repository at this point in the history
Through better reading of the spec, it seems that
unquoted values should be permissible in certain
circumstances, so we support parsing these as
symbols.
  • Loading branch information
simonwo committed Sep 15, 2020
1 parent ae79ab7 commit 2193048
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 20 deletions.
27 changes: 16 additions & 11 deletions lib/dbml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ 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

RESERVED_PUNCTUATION = %q{`"':\[\]\{\}\(\)\>\<,.}

# ATOM parses true: 'true' => true
# ATOM parses false: 'false' => false
# ATOM parses null: 'null' => nil
Expand All @@ -45,6 +47,8 @@ def self.block type, name_parser, content_parser, &block
NULL = 'null'.r.map {|_| nil }
NUMBER = prim(:double)
EXPRESSION = seq('`'.r, /[^`]+/.r, '`'.r)[1].map {|str| Expression.new str}
# KEYWORD parses phrases: 'no action' => :"no action"
KEYWORD = /[^#{RESERVED_PUNCTUATION}\s][^#{RESERVED_PUNCTUATION}]*/.r.map {|str| str.to_sym}
SINGLE_LING_STRING = seq("'".r, /[^']+/.r, "'".r)[1]
MULTI_LINE_STRING = seq("'''".r, /([^']|'[^']|''[^'])+/m.r, "'''".r)[1].map do |string|
# MULTI_LINE_STRING ignores indentation on the first line: "''' long\n string'''" => "long\n string"
Expand All @@ -61,11 +65,12 @@ def self.block type, name_parser, content_parser, &block
# Each setting item can take in 2 forms: Key: Value or keyword, similar to that of Python function parameters.
# Settings are all defined within square brackets: [setting1: value1, setting2: value2, setting3, setting4]
#
# SETTINGS parses key value settings: '[default: 123]' => {'default' => 123}
# SETTINGS parses keyword settings: '[not null]' => {'not null' => nil}
# SETTINGS parses many settings: "[some setting: 'value', primary key]" => {'some setting' => 'value', 'primary key' => nil}
SETTING = seq_(/[^,:\[\]\{\}\s][^,:\[\]]+/.r, (':'.r >> ATOM).maybe(&method(:unwrap))) {|(key, value)| {key => value} }
SETTINGS = ('['.r >> comma_separated(SETTING) << ']'.r).map {|values| values.reduce({}, &:update) }
# SETTINGS parses key value settings: '[default: 123]' => {default: 123}
# SETTINGS parses keyword settings: '[not null]' => {:'not null' => nil}
# SETTINGS parses many settings: "[some setting: 'value', primary key]" => {:'some setting' => 'value', :'primary key' => nil}
# SETTINGS parses keyword values: "[delete: cascade]" => {delete: :cascade}
SETTING = seq_(KEYWORD, (':'.r >> (ATOM | KEYWORD)).maybe(&method(:unwrap))) {|(key, value)| {key => value} }

# NOTE parses short notes: "Note: 'this is cool'" => 'this is cool'
# NOTE parses block notes: "Note {\n'still a single line of note'\n}" => 'still a single line of note'
Expand All @@ -87,7 +92,7 @@ def self.block type, name_parser, content_parser, &block
# created_at [note: 'Date']
# booking_date
# (country, booking_date) [unique]
# booking_date [type: 'hash']
# booking_date [type: hash]
# (`id*2`)
# (`id*3`,`getdate()`)
# (`id*3`,id)
Expand All @@ -105,11 +110,11 @@ def self.block type, name_parser, content_parser, &block
# INDEX parses composite fields: '(id, country)' => DBML::Index.new(['id', 'country'], {})
# INDEX parses expressions: '(`id*2`)' => DBML::Index.new([DBML::Expression.new('id*2')], {})
# INDEX parses expressions: '(`id*2`,`id*3`)' => DBML::Index.new([DBML::Expression.new('id*2'), DBML::Expression.new('id*3')], {})
# INDEX parses naked ids and settings: "test_col [type: 'hash']" => DBML::Index.new(["test_col"], {"type" => "hash"})
# INDEX parses settings: '(country, booking_date) [unique]' => DBML::Index.new(['country', 'booking_date'], {'unique' => nil})
# INDEX parses naked ids and settings: "test_col [type: hash]" => DBML::Index.new(["test_col"], {type: :hash})
# INDEX parses settings: '(country, booking_date) [unique]' => DBML::Index.new(['country', 'booking_date'], {unique: nil})
# INDEXES parses empty block: 'indexes { }' => []
# INDEXES parses single index: "indexes {\ncolumn_name\n}" => [DBML::Index.new(['column_name'], {})]
# INDEXES parses multiple indexes: "indexes {\n(composite) [pk]\ntest_index [unique]\n}" => [DBML::Index.new(['composite'], {'pk'=>nil}), DBML::Index.new(['test_index'], {'unique'=>nil})]
# INDEXES parses multiple indexes: "indexes {\n(composite) [pk]\ntest_index [unique]\n}" => [DBML::Index.new(['composite'], {pk: nil}), DBML::Index.new(['test_index'], {unique: nil})]

INDEX_SINGLE = /[^\(\)\,\{\}\s\[\]]+/.r
INDEX_COMPOSITE = seq_('('.r, comma_separated(EXPRESSION | INDEX_SINGLE), ')'.r).inner.map {|v| unwrap(v) }
Expand All @@ -131,8 +136,8 @@ def self.block type, name_parser, content_parser, &block
# }
#
# ENUM parses empty blocks: "enum empty {\n}" => DBML::Enum.new('empty', [])
# ENUM parses settings: "enum setting {\none [note: 'something']\n}" => DBML::Enum.new('setting', [DBML::EnumChoice.new('one', {'note' => 'something'})])
# ENUM parses filled blocks: "enum filled {\none\ntwo}" =? DBML::Enum.new('filled', [DBML::EnumChoice.new('one', {}), DBML::EnumChoice.new('two', {})])
# ENUM parses settings: "enum setting {\none [note: 'something']\n}" => DBML::Enum.new('setting', [DBML::EnumChoice.new('one', {note: 'something'})])

ENUM_CHOICE = seq_(/[^\{\}\s]+/.r, SETTINGS.maybe).map {|(name, settings)| EnumChoice.new name, unwrap(settings) }
ENUM = block 'enum', /\S+/.r, ENUM_CHOICE do |(name, choices)|
Expand All @@ -159,7 +164,7 @@ def self.block type, name_parser, content_parser, &block
# COLUMN parses naked identifiers as names: 'column_name type' => DBML::Column.new('column_name', 'type', {})
# COLUMN parses quoted identifiers as names: '"column name" type' => DBML::Column.new('column name', 'type', {})
# COLUMN parses types: 'name string' => DBML::Column.new('name', 'string', {})
# COLUMN parses settings: 'name string [pk]' => DBML::Column.new('name', 'string', {'pk' => nil})
# COLUMN parses settings: 'name string [pk]' => DBML::Column.new('name', 'string', {pk: nil})

QUOTED_COLUMN_NAME = '"'.r >> /[^"]+/.r << '"'.r
UNQUOTED_COLUMN_NAME = /[^\{\}\s]+/.r
Expand Down Expand Up @@ -221,8 +226,8 @@ def self.block type, name_parser, content_parser, &block
#
# PROJECT_DEFINITION parses names: 'Project my_proj { }' => DBML::ProjectDef.new('my_proj', [], {})
# PROJECT_DEFINITION parses notes: "Project my_porg { Note: 'porgs are cool!' }" => DBML::ProjectDef.new('my_porg', ['porgs are cool!'], {})
# PROJECT_DEFINITION parses settings: "Project my_cool {\ndatabase_type: 'PostgreSQL'\n}" => DBML::ProjectDef.new('my_cool', [], {'database_type' => 'PostgreSQL'})
PROJECT_DEFINITION = block 'Project', /\S+/.r, (NOTE | SETTING).star do |(name, objects)|
# PROJECT_DEFINITION parses settings: "Project my_cool {\ndatabase_type: 'PostgreSQL'\n}" => DBML::ProjectDef.new('my_cool', [], {database_type: 'PostgreSQL'})
ProjectDef.new name,
objects.select {|o| o.is_a? String },
objects.select {|o| o.is_a? Hash }.reduce({}, &:update)
Expand Down
18 changes: 9 additions & 9 deletions test/dbml_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,36 @@
assert_kind_of DBML::Project, proj
assert_equal DBML::Project.new("project_name",
["Description of the project"],
{"database_type"=>"PostgreSQL"},
{:"database_type"=>"PostgreSQL"},
[ # tables
DBML::Table.new("bookings", nil, [], [
DBML::Column.new("id", "integer", {}),
DBML::Column.new("country", "varchar", {}),
DBML::Column.new("booking_date", "date", {}),
DBML::Column.new("created_at", "timestamp", {})
], [
DBML::Index.new(["id", "country"], {"pk" => nil}),
DBML::Index.new(["created_at"], {"note" => 'Date'}),
DBML::Index.new(["id", "country"], {:"pk" => nil}),
DBML::Index.new(["created_at"], {:"note" => 'Date'}),
DBML::Index.new(["booking_date"], {}),
DBML::Index.new(["country", "booking_date"], {"unique" => nil}),
DBML::Index.new(["booking_date"], {"type" => "hash"}),
DBML::Index.new(["country", "booking_date"], {:"unique" => nil}),
DBML::Index.new(["booking_date"], {:"type" => :"hash"}),
DBML::Index.new([DBML::Expression.new("id*2")], {}),
DBML::Index.new([DBML::Expression.new("id*3"), DBML::Expression.new("getdate()")], {}),
DBML::Index.new([DBML::Expression.new("id*3"), "id"], {})
]),
DBML::Table.new("buildings", nil, [], [
DBML::Column.new("address", "varchar(255)", {"unique"=>nil, "not null"=>nil, "note"=>"to include unit number"}),
DBML::Column.new("id", "integer", {"pk"=>nil, "unique"=>nil, "default"=>123.0, "note"=>"Number"})
DBML::Column.new("address", "varchar(255)", {:"unique"=>nil, :"not null"=>nil, :"note"=>"to include unit number"}),
DBML::Column.new("id", "integer", {:"pk"=>nil, :"unique"=>nil, :"default"=>123.0, :"note"=>"Number"})
], []),
DBML::Table.new("table_name", nil, [], [
DBML::Column.new("column_name", "column_type", {"column_settings"=>nil})
], [])
DBML::Column.new("column_name", "column_type", {:"column_settings"=>nil})
], [ # enums
DBML::Enum.new("job_status", [
DBML::EnumChoice.new("created", {"note"=>"Waiting to be processed"}),
DBML::EnumChoice.new("running", nil),
DBML::EnumChoice.new("done", nil),
DBML::EnumChoice.new("failure", nil)
DBML::EnumChoice.new("created", {:"note"=>"Waiting to be processed"}),
])
], [ # table groups
DBML::TableGroup.new("tablegroup_name", [
Expand Down

0 comments on commit 2193048

Please sign in to comment.