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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4162a0c95318564102aa7e9f969807b3de0db5137f2b3d55147604d478cc330f
4
- data.tar.gz: df0d926928a2e898b315ba2193fdedc0480a964efc6722bba2d7dea46f2f3845
3
+ metadata.gz: 5f0d61e78928fa9a2632f02a2b8585b9b4c72f40e94607c82620d85e88dadf68
4
+ data.tar.gz: 693f15ca2c7605f91466675b69034a9564705e1da12d193248922805229b9fe8
5
5
  SHA512:
6
- metadata.gz: 40e2fd8ea32ba37f3bc443af646ce84e820f87db6cf81c38076dcbbf80d48963c03daff2bfe295a34aa5c03e012d9fddc7da8894b2a8bf88b3f1d0e5a5a3de6d
7
- data.tar.gz: 9c0155a8f48bbdd4e306456a5393a1736954b45dd960dfd32881a26058e5b9578bdc30a2182c515ec3c24c60e6d62a58a728790206a29b32eca5eabb1b4e9c24
6
+ metadata.gz: 5ae9931060de12c83439c1bede4abd32a0fabe67b22c87db03a17fb53f65371e301879731d76f18adbe806a4b0f6aa30a26ea40da7751329737d13df08e31f8a
7
+ data.tar.gz: 75872e72b6668ba506ab89b390b644ebfd308abfbe14716631005edb9d0e02887c3e6062f6d0bea08f9b7697d313da5ed8afbf6d064ee9077ac8d87fae3dc014
data/README.md CHANGED
@@ -1,15 +1,191 @@
1
- # Overture
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
- This is a library of tools and configurations based on the Clean Architecture.
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
- This gem contains the basic configuration options and file organization to create a Ruby application.
10
+ ---
6
11
 
7
- Sinatra is used as a delivery layer, but it can be easily replaced. Core business rules live in the Interactors.
12
+ ## Install it
13
+ If you're starting from scratch, you can install the gem manually by running:
8
14
 
9
- ## Contents
15
+ ```bash
16
+ $ gem install overture
17
+ ```
10
18
 
11
- - `Overture::Database`
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.
@@ -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])
@@ -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
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Templates
4
+ module_function
5
+
6
+ def [](name)
7
+ path = File.expand_path("templates/#{name}.erb", __dir__)
8
+ File.read(path)
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ puts 'Loading environment...'
5
+ require_relative '../config/environment'
6
+
7
+ require 'irb'
8
+ IRB.start
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bundle exec rerun -- puma -C config/puma.rb
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config/environment'
4
+ require 'http_api/endpoints'
5
+
6
+ run HttpApi::Endpoints
@@ -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,3 @@
1
+ web: bundle exec puma -C config/puma.rb
2
+ console: bin/console
3
+ release: bundle exec rake db:migrate
@@ -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
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_jsonapi'
4
+
5
+ module Overture
6
+ module HttpApi
7
+ class Serializer
8
+ include FastJsonapi::ObjectSerializer
9
+
10
+ set_id :id
11
+ end
12
+ end
13
+ 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
@@ -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
 
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Overture
4
+ class Serializer
5
+ end
6
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Overture
4
- VERSION = '0.1.6'
4
+ VERSION = '0.2.0'
5
5
  end
6
6
 
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.1.6
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-05-09 00:00:00.000000000 Z
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: tzinfo
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-data
112
+ name: tzinfo
85
113
  requirement: !ruby/object:Gem::Requirement
86
114
  requirements:
87
- - - "~>"
115
+ - - ">="
88
116
  - !ruby/object:Gem::Version
89
- version: '1.2018'
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: '1.2018'
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-migration
180
+ - overture
153
181
  extensions: []
154
182
  extra_rdoc_files: []
155
183
  files:
156
184
  - README.md
157
- - bin/overture-migration
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
- rubyforge_project:
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.
@@ -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
-