overture 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +185 -9
- data/bin/overture +87 -0
- data/generators/app.rb +113 -0
- data/generators/base.rb +55 -0
- data/generators/migration.rb +19 -0
- data/generators/templates.rb +10 -0
- data/generators/templates/bin_console.erb +8 -0
- data/generators/templates/bin_dev_http_api.erb +3 -0
- data/generators/templates/config_environment.erb +29 -0
- data/generators/templates/config_puma.erb +11 -0
- data/generators/templates/config_ru.erb +6 -0
- data/generators/templates/gemfile.erb +27 -0
- data/generators/templates/lib_http_api_endpoints.erb +24 -0
- data/generators/templates/lib_http_api_server.erb +47 -0
- data/generators/templates/migration.erb +17 -0
- data/generators/templates/procfile.erb +3 -0
- data/generators/templates/rakefile.erb +19 -0
- data/lib/overture/database.rb +3 -0
- data/lib/overture/http_api/helpers.rb +57 -0
- data/lib/overture/http_api/serializer.rb +13 -0
- data/lib/overture/http_api/server.rb +49 -0
- data/lib/overture/interactor.rb +42 -0
- data/lib/overture/serializer.rb +6 -0
- data/lib/overture/version.rb +1 -1
- metadata +58 -12
- data/bin/overture-migration +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f0d61e78928fa9a2632f02a2b8585b9b4c72f40e94607c82620d85e88dadf68
|
4
|
+
data.tar.gz: 693f15ca2c7605f91466675b69034a9564705e1da12d193248922805229b9fe8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ae9931060de12c83439c1bede4abd32a0fabe67b22c87db03a17fb53f65371e301879731d76f18adbe806a4b0f6aa30a26ea40da7751329737d13df08e31f8a
|
7
|
+
data.tar.gz: 75872e72b6668ba506ab89b390b644ebfd308abfbe14716631005edb9d0e02887c3e6062f6d0bea08f9b7697d313da5ed8afbf6d064ee9077ac8d87fae3dc014
|
data/README.md
CHANGED
@@ -1,15 +1,191 @@
|
|
1
|
-
|
1
|
+
> overture
|
2
|
+
> _noun_
|
3
|
+
>
|
4
|
+
> 1. an orchestral piece at the beginning of an opera, suite, play, oratorio, or other extended composition.
|
5
|
+
> 2. an introduction to something more substantial
|
2
6
|
|
3
|
-
|
7
|
+
Overture is the _start of something_, an introduction. So this gem provides an introduction to your new application (or service).
|
8
|
+
It helps you start developing your application so you can build a full orchestra eventually.
|
4
9
|
|
5
|
-
|
10
|
+
---
|
6
11
|
|
7
|
-
|
12
|
+
## Install it
|
13
|
+
If you're starting from scratch, you can install the gem manually by running:
|
8
14
|
|
9
|
-
|
15
|
+
```bash
|
16
|
+
$ gem install overture
|
17
|
+
```
|
10
18
|
|
11
|
-
|
12
|
-
- `Overture::Interactor`
|
13
|
-
- `Overture::Model`
|
14
|
-
- `Overture::PasswordDigester`
|
19
|
+
If you have an existing app and you want to use Overture, you can add it to your Gemfile:
|
15
20
|
|
21
|
+
```ruby
|
22
|
+
gem 'overture'
|
23
|
+
```
|
24
|
+
|
25
|
+
## What it can do?
|
26
|
+
|
27
|
+
Title says _can_ because you **can** choose what you need from Overture. You don't need to use everything.
|
28
|
+
If you only need a database connection adapter, just pull that. Or if you need a basic Sinatra configuration for a JSON API, just use that.
|
29
|
+
|
30
|
+
Overture is a set of tools or modules that you can use independently. All modules can interplay together as well, and they'll let you get something working pretty quickly.
|
31
|
+
|
32
|
+
> In most cases you only need the hammer to nail a nail.
|
33
|
+
|
34
|
+
### Creating an app
|
35
|
+
|
36
|
+
After you install the gem, you can run this command to generate an app directory layout:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
$ overture generate app my-app
|
40
|
+
```
|
41
|
+
|
42
|
+
The last argument is the name of the folder you want to use for your app. This will generate a directory structure like the following:
|
43
|
+
|
44
|
+
```
|
45
|
+
├── Gemfile
|
46
|
+
├── Gemfile.lock
|
47
|
+
├── Procfile
|
48
|
+
├── Rakefile
|
49
|
+
├── bin
|
50
|
+
│ ├── console
|
51
|
+
│ └── dev_http_api
|
52
|
+
├── config
|
53
|
+
│ ├── environment.rb
|
54
|
+
│ └── puma.rb
|
55
|
+
├── config.ru
|
56
|
+
└── lib
|
57
|
+
├── http_api
|
58
|
+
│ ├── endpoints.rb
|
59
|
+
│ └── server.rb
|
60
|
+
├── interactors
|
61
|
+
└── models
|
62
|
+
```
|
63
|
+
|
64
|
+
This command will create all necessary files to create a full application. This means you'll have three layers:
|
65
|
+
|
66
|
+
*Application logic*
|
67
|
+
|
68
|
+
All logic that is not specific to your domain (business), is handled here. In other words, this is where all your models (entities) live. Specifically in the `lib/models` directory.
|
69
|
+
|
70
|
+
*Business logic*
|
71
|
+
|
72
|
+
All the logic that is specific to your domain (business) goes here. This layer is handled by _Interactors_, also known as _Use Cases_. These classes are created in the `lib/interactors` directory.
|
73
|
+
|
74
|
+
*Presentation layer*
|
75
|
+
|
76
|
+
This is basically the output of your application that is used by other applications or services. This layer is managed by a Sinatra app that accepts and responds to JSON HTTP endpoints. The endpoints are defined in the `lib/http_api/endpoints.rb` file. You can also organize your endpoints in different files/classes, preferably subclasses of the `HttpApi::Server` class defined in the `lib/http_api/server.rb` file.
|
77
|
+
|
78
|
+
All other files outside of `lib` are practically configuration files. You can see what they do by opening them.
|
79
|
+
|
80
|
+
Once you create your app, you can run `bin/dev_http_api` to start up a Rack server. This will run in the port 3000 by default but you can change it in the `config/puma.rb` file or with the `PORT` env var.
|
81
|
+
|
82
|
+
#### Configuring via environment variables
|
83
|
+
|
84
|
+
Overture uses [dotenv](https://rubygems.org/gems/dotenv) to load environment variables defined in the `.env` file of your project.
|
85
|
+
|
86
|
+
> You should *NOT* version your `.env` file. Instead you can add a `.env.example` file with dummy values.
|
87
|
+
|
88
|
+
### Connecting to a database
|
89
|
+
|
90
|
+
You can use `Overture::Database` to create a connection to a database using Sequel.
|
91
|
+
|
92
|
+
Once you've generated your app structure, in your `config/environment.rb` file uncomment the line that requires the database module, and the one that creates the connection instance:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
require 'overture/database'
|
96
|
+
|
97
|
+
DB = Overture::Database.connect(ENV.fetch('DATABASE_URL'))
|
98
|
+
```
|
99
|
+
|
100
|
+
As you can see, it uses the environment variable `DATABASE_URL`. You can define it in your `.env` file (see [Configuring via environment variables](#configuring-via-environment-variables) section).
|
101
|
+
|
102
|
+
### Model the datasets
|
103
|
+
|
104
|
+
You can map your database tables to Ruby classes that inherit the `Overture::Model` class.
|
105
|
+
|
106
|
+
For example if you have a `users` table, you can create a `User` model like this:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class User < Overture::Model
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
This will automatically map to the `users` table following the Sequel convention for the tables naming as described [here](http://sequel.jeremyevans.net/rdoc/files/README_rdoc.html#label-Sequel+Models).
|
114
|
+
|
115
|
+
You now can persist and retrieve records in the `users` table:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
User.create(first_name: 'John')
|
119
|
+
user = User.first(first_name: 'John')
|
120
|
+
user.update(first_name: 'Juan')
|
121
|
+
```
|
122
|
+
|
123
|
+
Refer to the [Sequel documentation](http://sequel.jeremyevans.net/rdoc/files/doc/dataset_basics_rdoc.html) to see what methods are available.
|
124
|
+
|
125
|
+
#### Version your schema with migrations
|
126
|
+
|
127
|
+
To create the tables in your database you can do it manually or use migration files that will also help you revert to a specific version if needed.
|
128
|
+
|
129
|
+
In order to create a new migration file you run:
|
130
|
+
|
131
|
+
```bash
|
132
|
+
$ overture generate migration users
|
133
|
+
```
|
134
|
+
|
135
|
+
This will create a file under the `migrations` folder. Open it and change to your needs. See the [Sequel migration documentation](http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html) to know what you can do.
|
136
|
+
|
137
|
+
### Implement your business logic
|
138
|
+
|
139
|
+
As mentioned above, your business logic lis handled by _interactors_. An interactor is a subclass of the [Interactor class.](https://github.com/collectiveidea/interactor) with some methods added for convenience. See the [`Overture::Interactor`](lib/overture/interactor.rb) class to find what methods are added.
|
140
|
+
|
141
|
+
#### Digest and validate passwords
|
142
|
+
|
143
|
+
Overture comes with a password digester module that helps you digest and validate passwords.
|
144
|
+
|
145
|
+
To use it, include it in your class and call `digest_password(password)` or `valid_password?(password_digest, plain_password)` methods to digest or validate a password respectively.
|
146
|
+
|
147
|
+
For example:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
require 'overture/password_digester'
|
151
|
+
|
152
|
+
class AuthenticateUser < Overture::Interactor
|
153
|
+
include Overture::PasswordDigester
|
154
|
+
|
155
|
+
required :email, :password
|
156
|
+
|
157
|
+
def call
|
158
|
+
user = User.first(email: context.email)
|
159
|
+
fail!('user does not exist' unless user
|
160
|
+
context.authenticated = valid_password?(user.password_digest, context.password)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
### Communicate with the exterior
|
166
|
+
|
167
|
+
The `Overture::HttpApi::Server` class is a Sinatra app configured to accept and respond to JSON.
|
168
|
+
It includes the `Overture::HttpApi::Helpers` module too, which provides some common helpers.
|
169
|
+
|
170
|
+
When you generate an app with Overture, it creates a subclass of `Overture::HttpApi::Server` that you can configure to your needs.
|
171
|
+
|
172
|
+
You can define your endpoints in the `HttpApi::Endpoints` class. Or organize them in subclasses of `HttpApi::Server` that you can mount in the main `HttpApi::Endpoints` class.
|
173
|
+
|
174
|
+
#### Serializing your models
|
175
|
+
|
176
|
+
When you have a model class (i.e. `User`), and you want to present it as a JSON. You can use the `json()` helper method and it will automatically try to find a corresponding serializer for that class based on its name plus the `Serializer` suffix. In this case it'd be `UserSerializer`.
|
177
|
+
|
178
|
+
The `Overture::Serializer` produces [JSON:API](https://jsonapi.org/) object. And it's based in the [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi) serializer. So as long as you conform with its interface, you can create your own too.
|
179
|
+
|
180
|
+
If you have a serializer that does not conforms to the naming convention, you can specify what serializer to use in the `json()` helper method:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
get '/admin/users' do
|
184
|
+
users = User.all
|
185
|
+
json users, serializer: AdminUserSerializer
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
---
|
190
|
+
|
191
|
+
As mentioned in the beginning, you don't need to use all these components. You could for example only use the `Overture::Interactor` class, or the `Overture::PasswordDigester` module in your application.
|
data/bin/overture
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Cli
|
5
|
+
attr_reader :command
|
6
|
+
|
7
|
+
def initialize(command)
|
8
|
+
@command = command.downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.usage
|
12
|
+
require_relative '../lib/overture/version'
|
13
|
+
|
14
|
+
puts <<~HELP
|
15
|
+
Overture version: #{Overture::VERSION}
|
16
|
+
Usage: overture <command> <command-args>'
|
17
|
+
|
18
|
+
Commands:
|
19
|
+
|
20
|
+
generate
|
21
|
+
help
|
22
|
+
HELP
|
23
|
+
end
|
24
|
+
|
25
|
+
def usage
|
26
|
+
case command
|
27
|
+
when 'generate'
|
28
|
+
puts <<~HELP
|
29
|
+
Usage: overture generate <template>
|
30
|
+
|
31
|
+
Templates:
|
32
|
+
|
33
|
+
app <app-name> Creates an app directory structure
|
34
|
+
migration <name> Creates a migration file
|
35
|
+
HELP
|
36
|
+
else
|
37
|
+
self.class.usage
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def run!(*args)
|
42
|
+
case command
|
43
|
+
when 'generate'
|
44
|
+
generate!(*args)
|
45
|
+
else
|
46
|
+
usage
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def generate!(*args)
|
53
|
+
return usage if args.empty?
|
54
|
+
|
55
|
+
case args.shift.downcase
|
56
|
+
when 'app'
|
57
|
+
generate_app!(*args)
|
58
|
+
when 'migration'
|
59
|
+
generate_migration!(*args)
|
60
|
+
else
|
61
|
+
usage
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def generate_app!(*args)
|
66
|
+
return usage if args.empty?
|
67
|
+
|
68
|
+
require_relative '../generators/app'
|
69
|
+
|
70
|
+
generator = Overture::Generators::App.new
|
71
|
+
generator.call(app_name: args[0])
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_migration!(*args)
|
75
|
+
return usage if args.empty?
|
76
|
+
|
77
|
+
require_relative '../generators/migration'
|
78
|
+
|
79
|
+
generator = Overture::Generators::Migration.new
|
80
|
+
generator.call(name: args[0])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
return Cli.usage if ARGV.empty?
|
85
|
+
|
86
|
+
cli = Cli.new(ARGV[0])
|
87
|
+
cli.run!(*ARGV[1..-1])
|
data/generators/app.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'templates'
|
5
|
+
|
6
|
+
module Overture
|
7
|
+
module Generators
|
8
|
+
class App < Base
|
9
|
+
def call(vars = {})
|
10
|
+
@app_dir = vars[:app_name].gsub(' ', '_').downcase
|
11
|
+
|
12
|
+
create_files(vars)
|
13
|
+
run_setup
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :app_dir
|
19
|
+
|
20
|
+
def create_files(vars)
|
21
|
+
bin(vars)
|
22
|
+
config(vars)
|
23
|
+
lib(vars)
|
24
|
+
config_ru
|
25
|
+
gemfile
|
26
|
+
procfile
|
27
|
+
rakefile
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_setup
|
31
|
+
shell "cd #{app_dir} && bundle install"
|
32
|
+
end
|
33
|
+
|
34
|
+
def bin(vars)
|
35
|
+
bin_vars = vars.fetch(:bin) { {} }
|
36
|
+
|
37
|
+
bin_console(bin_vars)
|
38
|
+
bin_dev_http_api(bin_vars)
|
39
|
+
end
|
40
|
+
|
41
|
+
def config(vars)
|
42
|
+
config_vars = vars.fetch(:config) { {} }
|
43
|
+
|
44
|
+
config_environment(config_vars)
|
45
|
+
config_puma(config_vars)
|
46
|
+
end
|
47
|
+
|
48
|
+
def lib(vars)
|
49
|
+
lib_vars = vars.fetch(:lib) { {} }
|
50
|
+
|
51
|
+
mkdir("#{app_dir}/lib/interactors")
|
52
|
+
mkdir("#{app_dir}/lib/models")
|
53
|
+
|
54
|
+
lib_http_api_server(lib_vars)
|
55
|
+
lib_http_api_endpoints(lib_vars)
|
56
|
+
end
|
57
|
+
|
58
|
+
def config_ru(vars = {})
|
59
|
+
content = compile(Templates[:config_ru], vars)
|
60
|
+
write(content, "#{app_dir}/config.ru")
|
61
|
+
end
|
62
|
+
|
63
|
+
def gemfile(vars = {})
|
64
|
+
content = compile(Templates[:gemfile], vars)
|
65
|
+
write(content, "#{app_dir}/Gemfile")
|
66
|
+
end
|
67
|
+
|
68
|
+
def procfile(vars = {})
|
69
|
+
content = compile(Templates[:procfile], vars)
|
70
|
+
write(content, "#{app_dir}/Procfile")
|
71
|
+
end
|
72
|
+
|
73
|
+
def rakefile(vars = {})
|
74
|
+
content = compile(Templates[:rakefile], vars)
|
75
|
+
write(content, "#{app_dir}/Rakefile")
|
76
|
+
end
|
77
|
+
|
78
|
+
def bin_console(vars = {})
|
79
|
+
content = compile(Templates[:bin_console], vars)
|
80
|
+
|
81
|
+
path = write(content, "#{app_dir}/bin/console")
|
82
|
+
chmod('+x', path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def bin_dev_http_api(vars = {})
|
86
|
+
content = compile(Templates[:bin_dev_http_api], vars)
|
87
|
+
|
88
|
+
path = write(content, "#{app_dir}/bin/dev_http_api")
|
89
|
+
chmod('+x', path)
|
90
|
+
end
|
91
|
+
|
92
|
+
def config_environment(vars = {})
|
93
|
+
content = compile(Templates[:config_environment], vars)
|
94
|
+
write(content, "#{app_dir}/config/environment.rb")
|
95
|
+
end
|
96
|
+
|
97
|
+
def config_puma(vars = {})
|
98
|
+
content = compile(Templates[:config_puma], vars)
|
99
|
+
write(content, "#{app_dir}/config/puma.rb")
|
100
|
+
end
|
101
|
+
|
102
|
+
def lib_http_api_server(vars = {})
|
103
|
+
content = compile(Templates[:lib_http_api_server], vars)
|
104
|
+
write(content, "#{app_dir}/lib/http_api/server.rb")
|
105
|
+
end
|
106
|
+
|
107
|
+
def lib_http_api_endpoints(vars = {})
|
108
|
+
content = compile(Templates[:lib_http_api_endpoints], vars)
|
109
|
+
write(content, "#{app_dir}/lib/http_api/endpoints.rb")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/generators/base.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Overture
|
7
|
+
module Generators
|
8
|
+
class Base
|
9
|
+
def initialize(config = {})
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile(template, vars = {})
|
14
|
+
ERB.new(template).result_with_hash(vars)
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: check existence and ask for overwrite
|
18
|
+
def write(content, filepath)
|
19
|
+
location = File.expand_path(filepath)
|
20
|
+
dirname = File.dirname(location)
|
21
|
+
|
22
|
+
mkdir(dirname)
|
23
|
+
|
24
|
+
File.open(location, 'w') do |f|
|
25
|
+
f.puts content
|
26
|
+
end
|
27
|
+
|
28
|
+
puts " created file: #{filepath}"
|
29
|
+
filepath
|
30
|
+
end
|
31
|
+
|
32
|
+
def mkdir(path)
|
33
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def touch(filepath)
|
37
|
+
write('', filepath)
|
38
|
+
end
|
39
|
+
|
40
|
+
def chmod(mod, location)
|
41
|
+
FileUtils.chmod(mod, File.expand_path(location))
|
42
|
+
end
|
43
|
+
|
44
|
+
def shell(command)
|
45
|
+
command = command.join(' && ') if command.is_a?(Array)
|
46
|
+
puts " running: #{command}"
|
47
|
+
`#{command}`
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
attr_reader :config
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'templates'
|
5
|
+
|
6
|
+
module Overture
|
7
|
+
module Generators
|
8
|
+
class Migration < Base
|
9
|
+
def call(vars = {})
|
10
|
+
name = vars[:name].tr(' ', '_').downcase
|
11
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
12
|
+
location = File.join('migrations', "#{timestamp}_#{name}.rb")
|
13
|
+
content = compile(Templates[:migration], vars)
|
14
|
+
|
15
|
+
write(content, location)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] ||= 'development'
|
4
|
+
ENV['APP_ENV'] = ENV['RACK_ENV']
|
5
|
+
|
6
|
+
require 'bundler'
|
7
|
+
Bundler.require(:default, ENV['RACK_ENV'])
|
8
|
+
|
9
|
+
## Uncomment the modules you want to use:
|
10
|
+
#require 'overture/database'
|
11
|
+
#require 'overture/model'
|
12
|
+
#require 'overture/interactor'
|
13
|
+
#require 'overture/password_digester'
|
14
|
+
|
15
|
+
# `./lib` is added to the `$LOAD_PATH` so given a file in `lib/modesl/user.rb` you can do:
|
16
|
+
# require 'models/user'
|
17
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
18
|
+
|
19
|
+
## Uncomment if you're using the Database module:
|
20
|
+
#DB = Overture::Database.connect(ENV.fetch('DATABASE_URL'))
|
21
|
+
|
22
|
+
## This is to stream one row at time instead of all rows
|
23
|
+
## https://github.com/jeremyevans/sequel_pg#streaming
|
24
|
+
#if Sequel::Postgres.supports_streaming?
|
25
|
+
# DB.extension(:pg_streaming)
|
26
|
+
# DB.stream_all_queries = true
|
27
|
+
#end
|
28
|
+
|
29
|
+
Dir.glob('{models,interactors}/**/*.rb', base: 'lib').each { |file| require file }
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dotenv/load'
|
4
|
+
|
5
|
+
workers Integer(ENV['WEB_CONCURRENCY'] || 1)
|
6
|
+
threads_count = Integer(ENV['APP_MAX_THREADS'] || 1)
|
7
|
+
threads threads_count, threads_count
|
8
|
+
worker_timeout 20
|
9
|
+
|
10
|
+
port ENV['PORT'] || 3001
|
11
|
+
environment ENV['RACK_ENV'] || 'development'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
gem 'concurrent-ruby', '1.0.5'
|
8
|
+
gem 'overture', '~> 0.2'
|
9
|
+
gem 'puma', '~> 3.12'
|
10
|
+
gem 'rake', '~> 12.3'
|
11
|
+
gem 'yajl-ruby', '~> 1.4', require: 'yajl/json_gem'
|
12
|
+
|
13
|
+
group :development do
|
14
|
+
gem 'rerun', '~> 0.13'
|
15
|
+
gem 'rubocop', '~> 0.70'
|
16
|
+
end
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
gem 'database_cleaner', '~> 1.7'
|
20
|
+
gem 'minitest', '~> 5.11', require: 'minitest/autorun'
|
21
|
+
gem 'minitest-reporters', '~> 1.3'
|
22
|
+
gem 'rack-test', '~> 1.1', require: 'rack/test'
|
23
|
+
end
|
24
|
+
|
25
|
+
group :development, :test do
|
26
|
+
gem 'pry-byebug', '~> 3.7'
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'server'
|
4
|
+
|
5
|
+
module HttpApi
|
6
|
+
# You can define all your endpoints in this class
|
7
|
+
# or you can mount other Sinatra apps:
|
8
|
+
#
|
9
|
+
# class Admin < Server
|
10
|
+
# get '/admin/users' do
|
11
|
+
# # ...
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# class Endpoints < Server
|
16
|
+
# use Admin
|
17
|
+
# end
|
18
|
+
class Endpoints < Server
|
19
|
+
get '/' do
|
20
|
+
data = { hello: 'world' }
|
21
|
+
json data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'overture/http_api/server'
|
4
|
+
|
5
|
+
module HttpApi
|
6
|
+
class Server < Overture::HttpApi::Server
|
7
|
+
# You can handle how errors are reported here.
|
8
|
+
# For example with Bugsnag or other.
|
9
|
+
def notify_error(error)
|
10
|
+
puts "[HttpApi::Server Error] #{error.inspect}"
|
11
|
+
end
|
12
|
+
|
13
|
+
configure do
|
14
|
+
## Uncomment to add authorization middleware to
|
15
|
+
## certain endpoints:
|
16
|
+
##
|
17
|
+
## get '/admin/zone', auth: [:admin] do
|
18
|
+
## # ...
|
19
|
+
## end
|
20
|
+
#set :auth do |*roles|
|
21
|
+
# condition do
|
22
|
+
# halt_unauthorized! unless current_user && roles.any? do |role|
|
23
|
+
# current_user.role?(role)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#end
|
27
|
+
|
28
|
+
## Add rack-cors to your Gemfile and uncomment below to configure
|
29
|
+
#use Rack::Cors do
|
30
|
+
# allow do
|
31
|
+
# origins ENV['CORS_ALLOW_ORIGINS'].split(/, ?/)
|
32
|
+
# resource '*',
|
33
|
+
# methods: :any,
|
34
|
+
# headers: :any,
|
35
|
+
# credentials: false
|
36
|
+
# end
|
37
|
+
#end
|
38
|
+
end
|
39
|
+
|
40
|
+
configure :production, :staging do
|
41
|
+
## add rack-ssl to your Gemfile and uncomment
|
42
|
+
## this line to force HTTPS on production
|
43
|
+
## and staging environments
|
44
|
+
#use Rack::SSL
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sequel.migration do
|
4
|
+
table_name = :<%= name %>
|
5
|
+
|
6
|
+
up do
|
7
|
+
create_table(table_name) do
|
8
|
+
primary_key :id
|
9
|
+
DateTime :created_at, null: false
|
10
|
+
DateTime :updated_at, null: false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
down do
|
15
|
+
drop_table(table_name)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'dotenv/load'
|
6
|
+
|
7
|
+
## Uncomment to load database tasks (i.e. rake db:migrate)
|
8
|
+
#overture_spec = Gem::Specification.find_by_name 'overture'
|
9
|
+
#load "#{overture_spec.gem_dir}/lib/tasks/db.rake"
|
10
|
+
|
11
|
+
Rake::TestTask.new(test: 'db:test:reset') do |t|
|
12
|
+
t.libs << 'test'
|
13
|
+
t.libs << 'lib'
|
14
|
+
t.test_files = FileList['test/**/*_test.rb']
|
15
|
+
t.warning = false
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Run tests'
|
19
|
+
task default: :test
|
data/lib/overture/database.rb
CHANGED
@@ -7,6 +7,8 @@ Sequel.application_timezone = :local
|
|
7
7
|
Sequel.extension :datetime_parse_to_time
|
8
8
|
|
9
9
|
module Overture
|
10
|
+
# This is a simple class that represents a Database connection
|
11
|
+
# using Sequel.
|
10
12
|
module Database
|
11
13
|
DEFAULT_OPTIONS = {
|
12
14
|
extensions: %i[
|
@@ -19,6 +21,7 @@ module Overture
|
|
19
21
|
|
20
22
|
module_function
|
21
23
|
|
24
|
+
# Connects Sequel to database with the passed URL and options.
|
22
25
|
def connect(url, options = {})
|
23
26
|
Sequel.connect(url, DEFAULT_OPTIONS.merge(options))
|
24
27
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Overture
|
4
|
+
module HttpApi
|
5
|
+
module Helpers
|
6
|
+
def json(data, serializer: nil, options: {})
|
7
|
+
data = data.all if data.respond_to?(:all)
|
8
|
+
return JSON.generate(data, options) unless serializer
|
9
|
+
|
10
|
+
serializer.new(data, options).serialized_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def halt_not_found!
|
14
|
+
halt_error!(404, 'Not Found')
|
15
|
+
end
|
16
|
+
|
17
|
+
def halt_unauthorized!
|
18
|
+
halt_error!(401, 'Unauthorized')
|
19
|
+
end
|
20
|
+
|
21
|
+
def halt_not_acceptable!
|
22
|
+
halt_error!(406, 'Not Acceptable')
|
23
|
+
end
|
24
|
+
|
25
|
+
def halt_unsupported_media_type!
|
26
|
+
halt_error!(415, 'Unsupported Media Type')
|
27
|
+
end
|
28
|
+
|
29
|
+
def halt_unprocessable!(error)
|
30
|
+
halt_error!(422, error)
|
31
|
+
end
|
32
|
+
|
33
|
+
def halt_server_error!(error = 'Server Error')
|
34
|
+
halt_error!(500, error)
|
35
|
+
end
|
36
|
+
|
37
|
+
def halt_error!(status, title)
|
38
|
+
halt status, json(
|
39
|
+
errors: [{
|
40
|
+
status: status.to_s,
|
41
|
+
title: title
|
42
|
+
}]
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def require_params!(*names)
|
47
|
+
names.each do |name|
|
48
|
+
halt_unprocessable!("#{name} is required") unless params.key?(name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def found!(object)
|
53
|
+
object.nil? ? halt_not_found! : object
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'rack/parser'
|
5
|
+
require_relative 'helpers'
|
6
|
+
|
7
|
+
module Overture
|
8
|
+
module HttpApi
|
9
|
+
class Server < Sinatra::Base
|
10
|
+
helpers Helpers
|
11
|
+
|
12
|
+
MIME_TYPE = 'application/vnd.api+json'
|
13
|
+
|
14
|
+
def notify_error(error)
|
15
|
+
puts "[ERROR] #{error.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
configure do
|
19
|
+
enable :logging
|
20
|
+
enable :protection
|
21
|
+
disable :raise_errors
|
22
|
+
set :dump_errors, settings.development?
|
23
|
+
disable :show_exceptions
|
24
|
+
|
25
|
+
use Rack::Parser
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
content_type MIME_TYPE
|
30
|
+
|
31
|
+
non_charset = request.media_type_params.keys.any? { |k| k != 'charset' }
|
32
|
+
halt_unsupported_media_type! if request.media_type != MIME_TYPE || non_charset
|
33
|
+
halt_not_acceptable! unless request.preferred_type.entry == MIME_TYPE || request.options?
|
34
|
+
end
|
35
|
+
|
36
|
+
not_found do
|
37
|
+
halt_not_found!
|
38
|
+
end
|
39
|
+
|
40
|
+
error do
|
41
|
+
exception = env['sinatra.error']
|
42
|
+
|
43
|
+
notify_error(exception)
|
44
|
+
|
45
|
+
halt_server_error!(exception.message)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/overture/interactor.rb
CHANGED
@@ -4,10 +4,26 @@ require 'interactor'
|
|
4
4
|
require_relative 'interactor/failure'
|
5
5
|
|
6
6
|
module Overture
|
7
|
+
# This is a subclass of [`::Interactor`](https://github.com/collectiveidea/interactor)
|
8
|
+
# with some methods added for comodity.
|
7
9
|
class Interactor
|
8
10
|
include ::Interactor
|
9
11
|
|
10
12
|
class << self
|
13
|
+
# Mark context keys as required.
|
14
|
+
#
|
15
|
+
# If any of the specified keys are sent as `nil`, it will
|
16
|
+
# fail with a message like `'<key> is required'`.
|
17
|
+
#
|
18
|
+
# Possible options are:
|
19
|
+
#
|
20
|
+
# - `allow_empty` Allows empty values such as `[]`, `''`
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# class MyInteractor < Overture::Interactor
|
25
|
+
# required :first_name, :last_name, allow_empty: true
|
26
|
+
# end
|
11
27
|
def required(*input)
|
12
28
|
before do
|
13
29
|
opts = input.last.is_a?(Hash) ? input.pop : {}
|
@@ -24,9 +40,35 @@ module Overture
|
|
24
40
|
end
|
25
41
|
end
|
26
42
|
|
43
|
+
# Fails context and sets `context.error` attribute to the passed message.
|
44
|
+
#
|
45
|
+
# An optional hash can be passed that will get merged when failing.
|
27
46
|
def fail!(message, meta = {})
|
28
47
|
context.fail!(meta.merge(error: message))
|
29
48
|
end
|
49
|
+
|
50
|
+
# Checks for presence and non-emptiness
|
51
|
+
def blank?(value)
|
52
|
+
return true if value.nil?
|
53
|
+
|
54
|
+
value = value.strip if value.respond_to?(:strip)
|
55
|
+
value.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Slice the passed keys from context if they're present.
|
59
|
+
#
|
60
|
+
# When `include_nils` is false, `nil` values are ignored.
|
61
|
+
def context_slice(*keys, include_nils: false)
|
62
|
+
keys.each_with_object({}) do |key, memo|
|
63
|
+
next unless context_key?(key)
|
64
|
+
|
65
|
+
memo[key] = context[key] if !context[key].nil? || include_nils
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def context_key?(key)
|
70
|
+
context.to_h.key?(key.to_sym)
|
71
|
+
end
|
30
72
|
end
|
31
73
|
end
|
32
74
|
|
data/lib/overture/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: overture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Edgar JS
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bcrypt
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fast_jsonapi
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: interactor
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-parser
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.7'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.7'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: sequel_pg
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,7 +95,7 @@ dependencies:
|
|
67
95
|
- !ruby/object:Gem::Version
|
68
96
|
version: '1.0'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
98
|
+
name: sinatra
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
72
100
|
requirements:
|
73
101
|
- - "~>"
|
@@ -81,19 +109,19 @@ dependencies:
|
|
81
109
|
- !ruby/object:Gem::Version
|
82
110
|
version: '2.0'
|
83
111
|
- !ruby/object:Gem::Dependency
|
84
|
-
name: tzinfo
|
112
|
+
name: tzinfo
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
86
114
|
requirements:
|
87
|
-
- - "
|
115
|
+
- - ">="
|
88
116
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
117
|
+
version: '0'
|
90
118
|
type: :runtime
|
91
119
|
prerelease: false
|
92
120
|
version_requirements: !ruby/object:Gem::Requirement
|
93
121
|
requirements:
|
94
|
-
- - "
|
122
|
+
- - ">="
|
95
123
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
124
|
+
version: '0'
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
126
|
name: minitest
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -149,18 +177,37 @@ description: |
|
|
149
177
|
is used for the business rules.
|
150
178
|
email: edgar.js@gmail.com
|
151
179
|
executables:
|
152
|
-
- overture
|
180
|
+
- overture
|
153
181
|
extensions: []
|
154
182
|
extra_rdoc_files: []
|
155
183
|
files:
|
156
184
|
- README.md
|
157
|
-
- bin/overture
|
185
|
+
- bin/overture
|
186
|
+
- generators/app.rb
|
187
|
+
- generators/base.rb
|
188
|
+
- generators/migration.rb
|
189
|
+
- generators/templates.rb
|
190
|
+
- generators/templates/bin_console.erb
|
191
|
+
- generators/templates/bin_dev_http_api.erb
|
192
|
+
- generators/templates/config_environment.erb
|
193
|
+
- generators/templates/config_puma.erb
|
194
|
+
- generators/templates/config_ru.erb
|
195
|
+
- generators/templates/gemfile.erb
|
196
|
+
- generators/templates/lib_http_api_endpoints.erb
|
197
|
+
- generators/templates/lib_http_api_server.erb
|
198
|
+
- generators/templates/migration.erb
|
199
|
+
- generators/templates/procfile.erb
|
200
|
+
- generators/templates/rakefile.erb
|
158
201
|
- lib/overture.rb
|
159
202
|
- lib/overture/database.rb
|
203
|
+
- lib/overture/http_api/helpers.rb
|
204
|
+
- lib/overture/http_api/serializer.rb
|
205
|
+
- lib/overture/http_api/server.rb
|
160
206
|
- lib/overture/interactor.rb
|
161
207
|
- lib/overture/interactor/failure.rb
|
162
208
|
- lib/overture/model.rb
|
163
209
|
- lib/overture/password_digester.rb
|
210
|
+
- lib/overture/serializer.rb
|
164
211
|
- lib/overture/version.rb
|
165
212
|
- lib/tasks/db.rake
|
166
213
|
homepage: https://github.com/edgarjs/overture
|
@@ -182,8 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
229
|
- !ruby/object:Gem::Version
|
183
230
|
version: '0'
|
184
231
|
requirements: []
|
185
|
-
|
186
|
-
rubygems_version: 2.7.6
|
232
|
+
rubygems_version: 3.0.3
|
187
233
|
signing_key:
|
188
234
|
specification_version: 4
|
189
235
|
summary: Tools and configurations for your web app.
|
data/bin/overture-migration
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require 'optparse'
|
5
|
-
require 'fileutils'
|
6
|
-
|
7
|
-
class Parser
|
8
|
-
def self.parse(options = {})
|
9
|
-
opt_parser = OptionParser.new do |opts|
|
10
|
-
opts.banner = 'Usage: overture-migration [options]'
|
11
|
-
|
12
|
-
opts.on '-nNAME', '--name=NAME', 'Migration name' do |n|
|
13
|
-
options[:name] = n
|
14
|
-
end
|
15
|
-
|
16
|
-
opts.on '-h', '--help', 'Prints this message' do
|
17
|
-
puts opts
|
18
|
-
exit
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
opt_parser.parse!(options)
|
23
|
-
options
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
options = Parser.parse
|
28
|
-
|
29
|
-
if options[:name].nil?
|
30
|
-
Parser.parse(['--help'])
|
31
|
-
exit
|
32
|
-
end
|
33
|
-
|
34
|
-
def generate_migration(name)
|
35
|
-
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
36
|
-
filename = "migrations/#{timestamp}_#{name.tr(' ', '_').downcase}.rb"
|
37
|
-
|
38
|
-
FileUtils.mkdir_p('migrations')
|
39
|
-
File.open(filename, 'w') do |f|
|
40
|
-
f.puts <<~TEMPLATE
|
41
|
-
# frozen_string_literal: true
|
42
|
-
|
43
|
-
Sequel.migration do
|
44
|
-
table_name = :table_name # change this
|
45
|
-
|
46
|
-
up do
|
47
|
-
create_table(table_name) do
|
48
|
-
primary_key :id
|
49
|
-
DateTime :created_at, null: false
|
50
|
-
DateTime :updated_at, null: false
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
down do
|
55
|
-
drop_table(table_name)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
TEMPLATE
|
60
|
-
end
|
61
|
-
puts "-----------> Created db migration: #{filename}"
|
62
|
-
end
|
63
|
-
|
64
|
-
generate_migration(options[:name])
|
65
|
-
|