Skip to content

Commit 50a1c5d

Browse files
committed
initial release
0 parents  commit 50a1c5d

35 files changed

Lines changed: 2457 additions & 0 deletions

.editorconfig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[*.cr]
2+
charset = utf-8
3+
end_of_line = lf
4+
insert_final_newline = true
5+
indent_style = space
6+
indent_size = 2
7+
trim_trailing_whitespace = true

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/doc/
2+
/lib/
3+
/bin/
4+
/.shards/
5+
6+
# Libraries don't need dependency lock
7+
# Dependencies will be locked in application that uses them
8+
/shard.lock

.travis.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
language: crystal
2+
services:
3+
- postgresql
4+
script:
5+
- crystal spec
6+
- crystal docs
7+
deploy:
8+
provider: pages
9+
skip_cleanup: true
10+
github_token: $GITHUB_TOKEN
11+
on:
12+
branch: master
13+
local_dir: doc
14+
before_script:
15+
- psql -c 'create database core_test;' -U postgres
16+
- psql $DATABASE_URL < spec/migration.sql
17+
env:
18+
- DATABASE_URL=postgres://postgres@localhost:5432/core_test

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Vlad Faust
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Core
2+
3+
**C**rystal **O**bject **RE**lational Mapping you've been waiting for.
4+
5+
[![Build Status](https://travis-ci.org/vladfaust/core.cr.svg?branch=master)](https://travis-ci.org/vladfaust/core.cr) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://vladfaust.com/core.cr) [![Dependency Status](https://shards.rocks/badge/github/vladfaust/core.cr/status.svg)](https://shards.rocks/github/vladfaust/core.cr) [![GitHub release](https://img.shields.io/github/release/vladfaust/core.cr.svg)](https://github.com/vladfaust/core.cr/releases)
6+
7+
## About
8+
9+
Tired of [ActiveRecord](https://wikipedia.org/wiki/Active_record_pattern)'s magic? Forget it. It's time for real programming!
10+
11+
**Core** is inspired by [Crecto](https://github.com/Crecto/crecto) but more transparent and Crystal-ish.
12+
13+
## Installation
14+
15+
Add this to your application's `shard.yml`:
16+
17+
```yaml
18+
dependencies:
19+
core:
20+
github: vladfaust/core.cr
21+
version: ~> 0.1.0
22+
```
23+
24+
## Usage
25+
26+
Assuming following initial database migration:
27+
28+
```sql
29+
CREATE TABLE users(
30+
id SERIAL PRIMARY KEY,
31+
name VARCHAR(100) NOT NULL,
32+
created_at TIMESTAMPTZ NOT NULL,
33+
updated_at TIMESTAMPTZ
34+
);
35+
36+
CREATE TABLE posts(
37+
id SERIAL PRIMARY KEY,
38+
author_id INT NOT NULL REFERENCES users (id),
39+
content TEXT NOT NULL,
40+
created_at TIMESTAMPTZ NOT NULL,
41+
updated_at TIMESTAMPTZ
42+
);
43+
```
44+
45+
```crystal
46+
require "core"
47+
require "db"
48+
require "pg" # Or another driver
49+
50+
class User < Core::Model
51+
schema do
52+
primary_key :id
53+
field :name, String
54+
virtual_field :posts_count, Int64
55+
reference :posts, Array(Post), foreign_key: :author_id
56+
created_at_field :created_at
57+
updated_at_field :updated_at
58+
end
59+
60+
validation do
61+
errors.push({:name => "length must be >= 3"}) unless name.try &.size.>= 3
62+
end
63+
end
64+
65+
class Post < Core::Model
66+
schema do
67+
primary_key :id
68+
field :content, String
69+
reference :author, User, key: :author_id
70+
created_at_field :created_at
71+
updated_at_field :updated_at
72+
end
73+
end
74+
75+
db = DB.open(ENV["DATABASE_URL"])
76+
query_logger = Core::QueryLogger.new(STDOUT)
77+
78+
user_repo = Core::Repository(User).new(db, query_logger)
79+
post_repo = Core::Repository(Post).new(db, query_logger)
80+
81+
user = User.new(name: "Fo")
82+
user.valid? # => false
83+
user.errors # => [{:name => "length must be >= 3"}]
84+
user.name = "Foo"
85+
user.valid? # => true
86+
87+
user.id = user_repo.insert(user) # See ^1
88+
# INSERT INTO users (name, created_at) VALUES ($1, $2) RETURNING id
89+
90+
post = Post.new(author: user, content: "Foo Bar")
91+
post.id = post_repo.insert(post) # See ^1
92+
# INSERT INTO posts (author_id, content, created_at) VALUES ($1, $2, $3) RETURNING id
93+
94+
alias Query = Core::Query
95+
96+
posts = post_repo.query(Query(Post).where(author: user))
97+
# SELECT * FROM posts WHERE author_id = $1
98+
99+
posts.first.content # => "Foo Bar"
100+
101+
query = Query(User)
102+
.join(:posts)
103+
.select(:*, :"COUNT(posts.id) AS posts_count")
104+
.group_by(%i(users.id posts.id))
105+
.one
106+
user = user_repo.query(query).first
107+
# SELECT *, COUNT (posts.id) AS posts_count
108+
# FROM users JOIN posts AS posts ON posts.author_id = users.id
109+
# GROUP BY users.id, posts.id LIMIT 1
110+
111+
user.posts_count # => 1
112+
113+
user.name = "Bar"
114+
user.changes # => {:name => "Bar"}
115+
user_repo.update(user)
116+
# UPDATE users SET name = $1 WHERE id = $2 RETURNING id
117+
118+
post_repo.delete(posts.first)
119+
# DELETE FROM posts WHERE id = $1
120+
```
121+
122+
**^1:** Returning IDs is not working for PostgreSQL yet. See https://github.com/will/crystal-pg/issues/112.
123+
124+
## Contributing
125+
126+
1. Fork it ( https://github.com/vladfaust/core.cr/fork )
127+
2. Create your feature branch (git checkout -b my-new-feature)
128+
3. Commit your changes (git commit -am 'Add some feature')
129+
4. Push to the branch (git push origin my-new-feature)
130+
5. Create a new Pull Request
131+
132+
## Contributors
133+
134+
- [@vladfaust](https://github.com/vladfaust) Vlad Faust - creator, maintainer

shard.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: core
2+
version: 0.1.0
3+
4+
authors:
5+
- Vlad Faust <mail@vladfaust.com>
6+
7+
crystal: 0.23.1
8+
9+
license: MIT
10+
11+
dependencies:
12+
db:
13+
github: crystal-lang/crystal-db
14+
version: ~> 0.4.2
15+
time_format:
16+
github: vladfaust/time_format.cr
17+
version: ~> 0.1.0
18+
19+
development_dependencies:
20+
pg:
21+
github: will/crystal-pg
22+
version: ~> 0.13.3

spec/migration.sql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- Run this before tests:
2+
3+
DROP TABLE IF EXISTS posts;
4+
DROP TABLE IF EXISTS users;
5+
6+
CREATE TABLE users(
7+
id SERIAL PRIMARY KEY,
8+
referrer_id INT REFERENCES users (id),
9+
role INT NOT NULL DEFAULT 0,
10+
name VARCHAR(100) NOT NULL,
11+
created_at TIMESTAMPTZ NOT NULL,
12+
updated_at TIMESTAMPTZ
13+
);
14+
15+
CREATE TABLE posts(
16+
id SERIAL PRIMARY KEY,
17+
author_id INT NOT NULL REFERENCES users (id),
18+
editor_id INT REFERENCES users (id),
19+
content TEXT NOT NULL,
20+
created_at TIMESTAMPTZ NOT NULL,
21+
updated_at TIMESTAMPTZ
22+
);

spec/model_spec.cr

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
require "./spec_helper"
2+
3+
describe Core::Model do
4+
user = uninitialized User
5+
6+
it do
7+
user = User.new(id: 42, name: "Vlad Faust", posts_count: 146)
8+
end
9+
10+
describe "#db_fields" do
11+
it do
12+
user.db_fields.should eq({:id => 42, :name => "Vlad Faust", :created_at => nil, :updated_at => nil, :role => User::Role::User, :referrer_id => nil})
13+
end
14+
end
15+
16+
post = uninitialized Post
17+
18+
context "with references" do
19+
it "has reference properties" do
20+
post = Post.new(author: user, content: "Foobar")
21+
post.author.should eq user
22+
user.posts = [post]
23+
user.posts.not_nil!.should contain(post)
24+
end
25+
26+
describe "#db_fields" do
27+
it do
28+
post.db_fields.should eq({
29+
:id => nil,
30+
:author_id => 42,
31+
:editor_id => nil,
32+
:content => "Foobar",
33+
:created_at => nil,
34+
:updated_at => nil,
35+
})
36+
end
37+
end
38+
39+
context "referencing self" do
40+
referrer = uninitialized User
41+
referral = uninitialized User
42+
43+
it do
44+
referrer = User.new(id: 44)
45+
referral = User.new(referrer: referrer)
46+
referral.referrer.should eq referrer
47+
referrer.referrals = [referral]
48+
referrer.referrals.not_nil!.should contain(referral)
49+
end
50+
51+
describe "#db_fields" do
52+
it do
53+
referral.db_fields[:referrer_id].should eq 44
54+
end
55+
end
56+
end
57+
end
58+
59+
describe "#changes" do
60+
it "is initially empty" do
61+
user.changes.empty?.should eq true
62+
end
63+
64+
it "tracks changes" do
65+
user.name = "Updated User"
66+
user.changes.should eq({:name => "Updated User"})
67+
68+
user.name = "Twice Updated User"
69+
user.changes.should eq({:name => "Twice Updated User"})
70+
end
71+
end
72+
73+
describe "validation" do
74+
describe "#valid?" do
75+
it do
76+
Post.new.valid?.should eq true
77+
user.name = nil
78+
user.valid?.should eq false
79+
user.name = "foobar"
80+
user.valid?.should eq true
81+
end
82+
end
83+
84+
describe "#errors" do
85+
it do
86+
user.errors.empty?.should eq true
87+
user.name = nil
88+
user.valid?
89+
user.errors.should eq([{:name => "length must be > 3"}])
90+
end
91+
end
92+
end
93+
end

spec/query/group_by_spec.cr

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
require "../query_spec"
2+
3+
describe Query do
4+
describe "#group_by" do
5+
it do
6+
Query(User).group_by(:"foo.id", :"bar.id").to_s.should eq <<-SQL
7+
SELECT * FROM users GROUP BY foo.id, bar.id
8+
SQL
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)