Skip to content

Commit 188491b

Browse files
committed
feat: Atom is a class now
1 parent 37fc33c commit 188491b

25 files changed

Lines changed: 674 additions & 658 deletions

spec/repository_spec.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class MockDB
5656
end
5757
end
5858

59-
module Atom
59+
class Atom
6060
class Repository
6161
def initialize(@db : MockDB, @logger = Atom::Repository::Logger::Dummy.new)
6262
end

src/atom/model.cr

Lines changed: 102 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,117 @@
11
require "./model/**"
22

3-
# When included, allows to define mapping from DB to a model.
4-
#
5-
# Be sure to fill it thouroughly with all databse columns, otherwise errors may occure.
6-
#
7-
# **Example:**
8-
#
9-
# Given SQL:
10-
#
11-
# ```sql
12-
# CREATE TABLE users (
13-
# id SERIAL PRIMARY KEY,
14-
# name TEXT NOT NULL,
15-
# active BOOLEAN NOT NULL DEFAULT true,
16-
# age INT
17-
# );
18-
#
19-
# CREATE TABLE posts (
20-
# id SERIAL PRIMARY KEY,
21-
# author_id INT NOT NULL REFERENCES users (id),
22-
# content TEXT NOT NULL,
23-
# created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
24-
# updated_at TIMESTAMPTZ
25-
# );
26-
# ```
27-
#
28-
# A proper schema for it:
29-
#
30-
# ```
31-
# class User
32-
# include Atom::Model
33-
#
34-
# schema users do
35-
# pkey id : Int32
36-
# type name : String
37-
# type active : Bool = DB::Default
38-
# type age : Union(Int32 | Nil)
39-
# type posts : Array(Post) # Implicit reference
40-
# end
41-
# end
42-
#
43-
# class Post
44-
# include Atom::Model
45-
#
46-
# schema posts do
47-
# pkey id : Int32
48-
# type author : User, key: "author_id" # Explicit reference
49-
# type content : String
50-
# type created_at : Time = DB::Default
51-
# type updated_at : Union(Time | Nil)
52-
# end
53-
# end
54-
# ```
55-
#
56-
# Would also define special enums `Attribute` and `Reference`, e.g. `Attribute::Id` and `Reference::Author`.
57-
#
58-
# `Attribute` enums have `#key` method which returns table key for this attribute.
59-
#
60-
# `Reference` enums have multiple methods:
61-
#
62-
# - `#direct?` - whether is this reference direct (e.g. `true` for `Reference::Author`)
63-
# - `#foreign?` - whether is this reference foreign (e.g. `true` for `Reference::Posts`)
64-
# - `#table` - returns table name (e.g. `"users"` for `Reference::Author`)
65-
# - `#key` - returns table key (e.g. `"author_id"` for `Reference::Author`)
66-
# - `#foreign_key` - returns foreign table key (e.g. `"author_id"` for `Reference::Posts`)
67-
# - `#primary_key` - returns table key for this reference's primary key (e.g. `"id"` for both refrences in this case)
68-
module Atom::Model
69-
# Define mapping from DB to model (and vice-versa) for the *table*.
70-
macro schema(table, &block)
71-
MODEL_TABLE = {{table.id.stringify}}
3+
class Atom
4+
# When included, allows to define mapping from DB to a model.
5+
#
6+
# Be sure to fill it thouroughly with all databse columns, otherwise errors may occure.
7+
#
8+
# **Example:**
9+
#
10+
# Given SQL:
11+
#
12+
# ```sql
13+
# CREATE TABLE users (
14+
# id SERIAL PRIMARY KEY,
15+
# name TEXT NOT NULL,
16+
# active BOOLEAN NOT NULL DEFAULT true,
17+
# age INT
18+
# );
19+
#
20+
# CREATE TABLE posts (
21+
# id SERIAL PRIMARY KEY,
22+
# author_id INT NOT NULL REFERENCES users (id),
23+
# content TEXT NOT NULL,
24+
# created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
25+
# updated_at TIMESTAMPTZ
26+
# );
27+
# ```
28+
#
29+
# A proper schema for it:
30+
#
31+
# ```
32+
# class User
33+
# include Atom::Model
34+
#
35+
# schema users do
36+
# pkey id : Int32
37+
# type name : String
38+
# type active : Bool = DB::Default
39+
# type age : Union(Int32 | Nil)
40+
# type posts : Array(Post) # Implicit reference
41+
# end
42+
# end
43+
#
44+
# class Post
45+
# include Atom::Model
46+
#
47+
# schema posts do
48+
# pkey id : Int32
49+
# type author : User, key: "author_id" # Explicit reference
50+
# type content : String
51+
# type created_at : Time = DB::Default
52+
# type updated_at : Union(Time | Nil)
53+
# end
54+
# end
55+
# ```
56+
#
57+
# Would also define special enums `Attribute` and `Reference`, e.g. `Attribute::Id` and `Reference::Author`.
58+
#
59+
# `Attribute` enums have `#key` method which returns table key for this attribute.
60+
#
61+
# `Reference` enums have multiple methods:
62+
#
63+
# - `#direct?` - whether is this reference direct (e.g. `true` for `Reference::Author`)
64+
# - `#foreign?` - whether is this reference foreign (e.g. `true` for `Reference::Posts`)
65+
# - `#table` - returns table name (e.g. `"users"` for `Reference::Author`)
66+
# - `#key` - returns table key (e.g. `"author_id"` for `Reference::Author`)
67+
# - `#foreign_key` - returns foreign table key (e.g. `"author_id"` for `Reference::Posts`)
68+
# - `#primary_key` - returns table key for this reference's primary key (e.g. `"id"` for both refrences in this case)
69+
module Model
70+
# Define mapping from DB to model (and vice-versa) for the *table*.
71+
macro schema(table, &block)
72+
MODEL_TABLE = {{table.id.stringify}}
7273

73-
def self.table
74-
{{table.id.stringify}}
75-
end
74+
def self.table
75+
{{table.id.stringify}}
76+
end
7677

77-
MODEL_ATTRIBUTES = [] of NamedTuple
78-
MODEL_REFERENCES = [] of NamedTuple
78+
MODEL_ATTRIBUTES = [] of NamedTuple
79+
MODEL_REFERENCES = [] of NamedTuple
7980

80-
macro finished
81-
{{yield.id}}
81+
macro finished
82+
{{yield.id}}
8283

83-
define_initializer
84-
define_changes
85-
define_query_enums
86-
define_query_shortcuts
87-
define_db_mapping
84+
define_initializer
85+
define_changes
86+
define_query_enums
87+
define_query_shortcuts
88+
define_db_mapping
89+
end
8890
end
89-
end
9091

91-
# Will be defined after `.schema` call. It would accept named arguments only, e.g. `User.new(id: 42)`.
92-
abstract def initialize(**nargs)
92+
# Will be defined after `.schema` call. It would accept named arguments only, e.g. `User.new(id: 42)`.
93+
abstract def initialize(**nargs)
9394

94-
# A storage for instance changes; will be defined after `.schema` call. Would not track foreign references changes.
95-
abstract def changes
95+
# A storage for instance changes; will be defined after `.schema` call. Would not track foreign references changes.
96+
abstract def changes
9697

97-
# Method to map instances from `DB::ResultSet`; will be defined after `.schema` call.
98-
def self.from_rs : Array(self)
99-
{% raise NotImplementedError %}
100-
end
98+
# Method to map instances from `DB::ResultSet`; will be defined after `.schema` call.
99+
def self.from_rs : Array(self)
100+
{% raise NotImplementedError %}
101+
end
101102

102-
# Would return a `Schema::Attribute` for shema's primary key; will be defined after `.schema` call.
103-
def self.primary_key
104-
{% raise NotImplementedError %}
105-
end
103+
# Would return a `Schema::Attribute` for shema's primary key; will be defined after `.schema` call.
104+
def self.primary_key
105+
{% raise NotImplementedError %}
106+
end
106107

107-
# Would safely return instance primary key or `nil` if not set; will be defined after `.schema` call.
108-
abstract def primary_key?
108+
# Would safely return instance primary key or `nil` if not set; will be defined after `.schema` call.
109+
abstract def primary_key?
109110

110-
# Would return instance primary key or raise `ArgumentError` if not set; will be defined after `.schema` call.
111-
abstract def primary_key
111+
# Would return instance primary key or raise `ArgumentError` if not set; will be defined after `.schema` call.
112+
abstract def primary_key
112113

113-
# Would check two instances for equality by their `primary_key` values; will be defined after `.schema` call. Would raise `ArgumentError` if any of instances had primary key value not set.
114-
abstract def ==(other : self)
114+
# Would check two instances for equality by their `primary_key` values; will be defined after `.schema` call. Would raise `ArgumentError` if any of instances had primary key value not set.
115+
abstract def ==(other : self)
116+
end
115117
end

src/atom/model/changes.cr

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
module Atom::Model
2-
# Define `changes` getter for this schema. It will track all changes made to instance's attributes, be it a scalar attribute or a reference.
3-
private macro define_changes
4-
{% types = MODEL_ATTRIBUTES.map(&.["type"]) + MODEL_REFERENCES.select(&.["direct"]).map(&.["type"]) %}
5-
{% types = types + [DB::Default.class] if (MODEL_ATTRIBUTES + MODEL_REFERENCES.select(&.["direct"])).find(&.["db_default"]) %}
1+
class Atom
2+
module Model
3+
# Define `changes` getter for this schema. It will track all changes made to instance's attributes, be it a scalar attribute or a reference.
4+
private macro define_changes
5+
{% types = MODEL_ATTRIBUTES.map(&.["type"]) + MODEL_REFERENCES.select(&.["direct"]).map(&.["type"]) %}
6+
{% types = types + [DB::Default.class] if (MODEL_ATTRIBUTES + MODEL_REFERENCES.select(&.["direct"])).find(&.["db_default"]) %}
67

7-
# A storage for changes, empty on initialization. To reset use `changes.clear`.
8-
getter changes = Hash(String, {{types.join(" | ").id}}).new
8+
# A storage for changes, empty on initialization. To reset use `changes.clear`.
9+
getter changes = Hash(String, {{types.join(" | ").id}}).new
910

10-
{% for type in MODEL_ATTRIBUTES + MODEL_REFERENCES.select(&.["direct"]) %}
11-
# :nodoc:
12-
def {{type["name"]}}=(value : {{type["type"]}}{{" | DB::Default.class".id if type["db_default"]}})
13-
changes[{{type["name"].stringify}}] = value unless @{{type["name"]}} == value
14-
@{{type["name"]}} = value
15-
end
16-
{% end %}
11+
{% for type in MODEL_ATTRIBUTES + MODEL_REFERENCES.select(&.["direct"]) %}
12+
# :nodoc:
13+
def {{type["name"]}}=(value : {{type["type"]}}{{" | DB::Default.class".id if type["db_default"]}})
14+
changes[{{type["name"].stringify}}] = value unless @{{type["name"]}} == value
15+
@{{type["name"]}} = value
16+
end
17+
{% end %}
18+
end
1719
end
1820
end

0 commit comments

Comments
 (0)