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 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
-