overture 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|