From 083170abc6a95e87d587744d4f6649fc7e35eec4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Oct 2022 11:59:24 +0100 Subject: [PATCH] Add basic structure --- .gitignore | 9 +++ Rakefile | 5 ++ Readme.md | 56 +++++++++++++++++++ bin/console | 15 +++++ bin/pico_api | 14 +++++ bin/setup | 8 +++ lib/pico_api.rb | 30 ++++++++++ lib/pico_api/configuration.rb | 30 ++++++++++ lib/pico_api/database.rb | 24 ++++++++ lib/pico_api/entities/error.rb | 22 ++++++++ lib/pico_api/entities/errors.rb | 22 ++++++++ lib/pico_api/generators/commands/base.rb | 45 +++++++++++++++ .../generators/commands/copy_template.rb | 19 +++++++ .../generators/commands/create_base.rb | 29 ++++++++++ .../commands/create_config_application.rb | 19 +++++++ .../generators/commands/create_config_boot.rb | 19 +++++++ .../commands/create_config_configuration.rb | 19 +++++++ .../commands/create_config_database_setup.rb | 19 +++++++ .../commands/create_config_database_yml.rb | 19 +++++++ .../commands/create_config_dotenv.rb | 19 +++++++ .../generators/commands/create_config_ru.rb | 19 +++++++ .../generators/commands/create_gemfile.rb | 19 +++++++ .../generators/commands/create_gitignore.rb | 19 +++++++ .../generators/commands/create_rakefile.rb | 19 +++++++ .../generators/commands/create_template.rb | 21 +++++++ lib/pico_api/generators/generator.rb | 40 +++++++++++++ lib/pico_api/generators/templates/.gitignore | 2 + lib/pico_api/generators/templates/Gemfile.erb | 7 +++ .../generators/templates/Rakefile.erb | 9 +++ .../generators/templates/config.ru.erb | 3 + .../templates/config/application.erb | 5 ++ .../generators/templates/config/boot.erb | 9 +++ .../templates/config/configuration.erb | 13 +++++ .../generators/templates/config/database.yml | 8 +++ .../templates/config/database_setup.erb | 6 ++ .../generators/templates/config/dotenv.erb | 6 ++ lib/pico_api/handlers/errors.rb | 23 ++++++++ lib/pico_api/json_api_renderer.rb | 11 ++++ lib/pico_api/serializers/base.rb | 11 ++++ lib/pico_api/version.rb | 5 ++ pico_api.gemspec | 33 +++++++++++ 41 files changed, 730 insertions(+) create mode 100644 .gitignore create mode 100644 Rakefile create mode 100644 Readme.md create mode 100755 bin/console create mode 100755 bin/pico_api create mode 100644 bin/setup create mode 100644 lib/pico_api.rb create mode 100644 lib/pico_api/configuration.rb create mode 100644 lib/pico_api/database.rb create mode 100644 lib/pico_api/entities/error.rb create mode 100644 lib/pico_api/entities/errors.rb create mode 100644 lib/pico_api/generators/commands/base.rb create mode 100644 lib/pico_api/generators/commands/copy_template.rb create mode 100644 lib/pico_api/generators/commands/create_base.rb create mode 100644 lib/pico_api/generators/commands/create_config_application.rb create mode 100644 lib/pico_api/generators/commands/create_config_boot.rb create mode 100644 lib/pico_api/generators/commands/create_config_configuration.rb create mode 100644 lib/pico_api/generators/commands/create_config_database_setup.rb create mode 100644 lib/pico_api/generators/commands/create_config_database_yml.rb create mode 100644 lib/pico_api/generators/commands/create_config_dotenv.rb create mode 100644 lib/pico_api/generators/commands/create_config_ru.rb create mode 100644 lib/pico_api/generators/commands/create_gemfile.rb create mode 100644 lib/pico_api/generators/commands/create_gitignore.rb create mode 100644 lib/pico_api/generators/commands/create_rakefile.rb create mode 100644 lib/pico_api/generators/commands/create_template.rb create mode 100644 lib/pico_api/generators/generator.rb create mode 100644 lib/pico_api/generators/templates/.gitignore create mode 100644 lib/pico_api/generators/templates/Gemfile.erb create mode 100644 lib/pico_api/generators/templates/Rakefile.erb create mode 100644 lib/pico_api/generators/templates/config.ru.erb create mode 100644 lib/pico_api/generators/templates/config/application.erb create mode 100644 lib/pico_api/generators/templates/config/boot.erb create mode 100644 lib/pico_api/generators/templates/config/configuration.erb create mode 100644 lib/pico_api/generators/templates/config/database.yml create mode 100644 lib/pico_api/generators/templates/config/database_setup.erb create mode 100644 lib/pico_api/generators/templates/config/dotenv.erb create mode 100644 lib/pico_api/handlers/errors.rb create mode 100644 lib/pico_api/json_api_renderer.rb create mode 100644 lib/pico_api/serializers/base.rb create mode 100644 lib/pico_api/version.rb create mode 100644 pico_api.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6216f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.gem diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..fb65a1f --- /dev/null +++ b/Rakefile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' + +task default: :spec diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..da0df6d --- /dev/null +++ b/Readme.md @@ -0,0 +1,56 @@ +# Pico API + +A tiny boilerplate JSON API template in ruby. Its minimal footprint makes it ideal for microservices or even full blown APIs. +It has been created for those who don't need the RAILS "magic" and bloat. Can easily create different architectures, like STAR, CQRS..., and MVC. + +Pico API is just a collection of the fastest ruby libraries put together to form a basic API environment, with the JSON:API specification. It doesn't create any folder structures for your business logic, it only creates the initial configurations to get you started. + +Example demo project can be found [here](https://github.com/alexavlonitis/pico_api_example) + +## Libraries Used + +- [roda](https://github.com/jeremyevans/roda) -> Routing +- [rom-rb](https://github.com/rom-rb/rom) -> ORM +- [jsonapi serializer](https://github.com/jsonapi-serializer/jsonapi-serializer) -> Fast-jsonapi fork + +## Usage + +### Create your app + +```ruby +gem install pico_api + +pico_api --new my_app + +cd my_app + +bundle install +``` + +### Configure your database + +Migration info: https://rom-rb.org/5.0/learn/sql/migrations/ + +- Add the database config details in `config/database.yml` +- Setup the Database: `rake db:setup` +- Create a migration: `rake db:create_migration[create_users]` (include your preferred DB gem in the Gemfile) +- Run the migrations: `rake db:migrate` +- Run the server: `rackup -p 3001` + + +## Development +- [x] Create Database config +- [x] Hook a json-api library +- [x] Handle Errors +- [ ] Create a Logger config +- [ ] Create a testing environment +- [ ] Allow multiple db gateways in the config +- [ ] Add irb/pry console script + +## Contributing + +All Pull Requests are welcome, from a single typo to a new feature. + +- Fork this repo +- Create a new branch, add your changes with tests when necessary +- Submit a Pull Request diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..e0bd9f4 --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'pico_api' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/pico_api b/bin/pico_api new file mode 100755 index 0000000..d46d136 --- /dev/null +++ b/bin/pico_api @@ -0,0 +1,14 @@ + +#!/usr/bin/env ruby + +require 'optparse' +require 'pico_api' + +options = {} +OptionParser.new do |opts| + opts.banner = "Usage: pico_api [options]" + + opts.on('-n', '--new project_name') { |o| options[:project_name] = o } +end.parse! + +PicoApi::Generators::Generator.call(options[:project_name]) diff --git a/bin/setup b/bin/setup new file mode 100644 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/pico_api.rb b/lib/pico_api.rb new file mode 100644 index 0000000..34605c0 --- /dev/null +++ b/lib/pico_api.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'roda' +require 'zeitwerk' + +loader = Zeitwerk::Loader.for_gem +loader.setup + +module PicoApi + def self.root_path + File.dirname(__dir__) + end + + def self.lib_path + File.join(root_path, 'lib/pico_api') + end + + class Application < Roda + parser = proc { |data| JSON.parse(data, symbolize_names: true) } + + plugin :symbol_status + plugin :json_parser, parser: parser + plugin :json, content_type: 'application/vnd.api+json' + plugin :error_handler do |e| + PicoApi::Handlers::Errors.call(e, response) + end + end +end + +loader.eager_load diff --git a/lib/pico_api/configuration.rb b/lib/pico_api/configuration.rb new file mode 100644 index 0000000..c04f776 --- /dev/null +++ b/lib/pico_api/configuration.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'erb' +require 'yaml' + +module PicoApi + class << self + def configuration + @configuration ||= Configuration.new(db_config) + end + + def db_config + template = ERB.new(File.new('./config/database.yml').read) + YAML.safe_load(template.result(binding)) + end + + def configure + yield(configuration) + end + end + + class Configuration + attr_reader :db_config + attr_accessor :namespace, :lib_path, :errors_map + + def initialize(db_config) + @db_config = db_config + end + end +end diff --git a/lib/pico_api/database.rb b/lib/pico_api/database.rb new file mode 100644 index 0000000..613074e --- /dev/null +++ b/lib/pico_api/database.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rom' +require 'rom-sql' + +module PicoApi + class Database + class << self + attr_accessor :container + + def setup! + database_config = PicoApi.configuration.db_config['default'] + adapter = database_config['adapter'].to_sym + options = database_config['options'].symbolize_keys + connection_string = database_config['connection_string'] + + config = ROM::Configuration.new(adapter, connection_string, options) + yield config if block_given? + + @container = ROM.container(config) + end + end + end +end diff --git a/lib/pico_api/entities/error.rb b/lib/pico_api/entities/error.rb new file mode 100644 index 0000000..6f9bba6 --- /dev/null +++ b/lib/pico_api/entities/error.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'securerandom' + +module PicoApi + module Entities + class Error < ROM::Struct + attribute? :title, ROM::Types::String + attribute? :status, ROM::Types::String.optional + attribute? :code, ROM::Types::String.optional + attribute? :detail, ROM::Types::String.optional + + def to_h + id.merge(super) + end + + def id + { id: SecureRandom.hex(5) } + end + end + end +end diff --git a/lib/pico_api/entities/errors.rb b/lib/pico_api/entities/errors.rb new file mode 100644 index 0000000..d3d79d8 --- /dev/null +++ b/lib/pico_api/entities/errors.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module PicoApi + module Entities + class Errors < ROM::Struct + class << self + def call(error) + class_name = error.class.name.demodulize + error_entities = [ + Entities::Error.new( + title: class_name.underscore, + detail: error.message + ) + ] + new(errors: error_entities) + end + end + + attribute? :errors, ROM::Types::Array(Entities::Error) + end + end +end diff --git a/lib/pico_api/generators/commands/base.rb b/lib/pico_api/generators/commands/base.rb new file mode 100644 index 0000000..a51f136 --- /dev/null +++ b/lib/pico_api/generators/commands/base.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'erb' + +module PicoApi + module Generators + module Commands + class Base + def self.call(project_name) + new(project_name).call + end + + def initialize(project_name) + @project_name_camelcased = project_name.camelize + @project_name_snakecased = project_name.underscore + end + + def call + NotImplementError + end + + def get_binding + binding + end + + private + + attr_reader :project_name_camelised, :project_name_snakecased + + def erb + ERB.new(File.read(template_full_path)) + end + + def template_full_path + File.join(PicoApi.lib_path, template_relative_path) + end + + def template_relative_path + '' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/copy_template.rb b/lib/pico_api/generators/commands/copy_template.rb new file mode 100644 index 0000000..65ed688 --- /dev/null +++ b/lib/pico_api/generators/commands/copy_template.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CopyTemplate < Base + def call + copy_file + end + + private + + def copy_file + FileUtils.cp(template_full_path, "#{project_name_snakecased}#{destination_path}") + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_base.rb b/lib/pico_api/generators/commands/create_base.rb new file mode 100644 index 0000000..cc68656 --- /dev/null +++ b/lib/pico_api/generators/commands/create_base.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateBase < Base + def call + create_bin_folder + create_lib_folder + create_config_folder + end + + private + + def create_bin_folder + FileUtils.mkdir_p("#{project_name_snakecased}/bin") + end + + def create_lib_folder + FileUtils.mkdir_p("#{project_name_snakecased}/lib/#{project_name_snakecased}") + end + + def create_config_folder + FileUtils.mkdir_p("#{project_name_snakecased}/config") + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_application.rb b/lib/pico_api/generators/commands/create_config_application.rb new file mode 100644 index 0000000..4505b45 --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_application.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigApplication < CreateTemplate + private + + def destination_path + '/config/application.rb' + end + + def template_relative_path + '/generators/templates/config/application.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_boot.rb b/lib/pico_api/generators/commands/create_config_boot.rb new file mode 100644 index 0000000..9c1e472 --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_boot.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigBoot < CopyTemplate + private + + def destination_path + '/config/boot.rb' + end + + def template_relative_path + '/generators/templates/config/boot.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_configuration.rb b/lib/pico_api/generators/commands/create_config_configuration.rb new file mode 100644 index 0000000..bba3d6e --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_configuration.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigConfiguration < CreateTemplate + private + + def destination_path + '/config/configuration.rb' + end + + def template_relative_path + '/generators/templates/config/configuration.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_database_setup.rb b/lib/pico_api/generators/commands/create_config_database_setup.rb new file mode 100644 index 0000000..a5ce0e4 --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_database_setup.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigDatabaseSetup < CopyTemplate + private + + def destination_path + '/config/database_setup.rb' + end + + def template_relative_path + '/generators/templates/config/database_setup.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_database_yml.rb b/lib/pico_api/generators/commands/create_config_database_yml.rb new file mode 100644 index 0000000..76246ca --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_database_yml.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigDatabaseYml < CopyTemplate + private + + def destination_path + '/config/database.yml' + end + + def template_relative_path + '/generators/templates/config/database.yml' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_dotenv.rb b/lib/pico_api/generators/commands/create_config_dotenv.rb new file mode 100644 index 0000000..7e81fa0 --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_dotenv.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigDotenv < CopyTemplate + private + + def destination_path + '/config/dotenv.rb' + end + + def template_relative_path + '/generators/templates/config/dotenv.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_config_ru.rb b/lib/pico_api/generators/commands/create_config_ru.rb new file mode 100644 index 0000000..1037553 --- /dev/null +++ b/lib/pico_api/generators/commands/create_config_ru.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateConfigRu < CreateTemplate + private + + def destination_path + '/config.ru' + end + + def template_relative_path + '/generators/templates/config.ru.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_gemfile.rb b/lib/pico_api/generators/commands/create_gemfile.rb new file mode 100644 index 0000000..20f1451 --- /dev/null +++ b/lib/pico_api/generators/commands/create_gemfile.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateGemfile < CopyTemplate + private + + def destination_path + '/Gemfile' + end + + def template_relative_path + '/generators/templates/Gemfile.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_gitignore.rb b/lib/pico_api/generators/commands/create_gitignore.rb new file mode 100644 index 0000000..bcc2256 --- /dev/null +++ b/lib/pico_api/generators/commands/create_gitignore.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateGitignore < CopyTemplate + private + + def destination_path + '/.gitignore' + end + + def template_relative_path + '/generators/templates/.gitignore' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_rakefile.rb b/lib/pico_api/generators/commands/create_rakefile.rb new file mode 100644 index 0000000..af276a4 --- /dev/null +++ b/lib/pico_api/generators/commands/create_rakefile.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateRakefile < CopyTemplate + private + + def destination_path + '/Rakefile' + end + + def template_relative_path + '/generators/templates/Rakefile.erb' + end + end + end + end +end diff --git a/lib/pico_api/generators/commands/create_template.rb b/lib/pico_api/generators/commands/create_template.rb new file mode 100644 index 0000000..f4b9d01 --- /dev/null +++ b/lib/pico_api/generators/commands/create_template.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + module Commands + class CreateTemplate < Base + def call + create_file + end + + private + + def create_file + File.open("#{project_name_snakecased}#{destination_path}", 'w') do |f| + f.write(erb.result(get_binding)) + end + end + end + end + end +end diff --git a/lib/pico_api/generators/generator.rb b/lib/pico_api/generators/generator.rb new file mode 100644 index 0000000..4024f2c --- /dev/null +++ b/lib/pico_api/generators/generator.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module PicoApi + module Generators + class Generator + class << self + def call(project_name) + commands = load_commands + new(project_name, commands).call + end + + def load_commands + skip_commands = %w[base copytemplate createtemplate] + + Dir[File.join(PicoApi.lib_path, '/generators/commands/*.rb')].sort.map do |file_path| + file_name = file_path.split('/').last.gsub('.rb', '').camelize + next if skip_commands.include?(file_name.downcase) + + "PicoApi::Generators::Commands::#{file_name}".constantize + end.compact + end + end + + def initialize(project_name, commands = []) + raise 'Missing project name' unless project_name + + @project_name = project_name + @commands = commands + end + + def call + commands.each { |command| command.call(project_name) } + end + + private + + attr_reader :project_name, :commands + end + end +end diff --git a/lib/pico_api/generators/templates/.gitignore b/lib/pico_api/generators/templates/.gitignore new file mode 100644 index 0000000..156fc15 --- /dev/null +++ b/lib/pico_api/generators/templates/.gitignore @@ -0,0 +1,2 @@ +.env +*.db diff --git a/lib/pico_api/generators/templates/Gemfile.erb b/lib/pico_api/generators/templates/Gemfile.erb new file mode 100644 index 0000000..cb210b5 --- /dev/null +++ b/lib/pico_api/generators/templates/Gemfile.erb @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +gem "pico_api" + +group :development, :test do + gem "dotenv" +end diff --git a/lib/pico_api/generators/templates/Rakefile.erb b/lib/pico_api/generators/templates/Rakefile.erb new file mode 100644 index 0000000..1c9ca40 --- /dev/null +++ b/lib/pico_api/generators/templates/Rakefile.erb @@ -0,0 +1,9 @@ +require "pico_api" +require "./config/dotenv" +require 'rom/sql/rake_task' + +namespace :db do + task :setup do + ROM::SQL::RakeSupport.env = ROM.container(default: [:sql, ENV.fetch("DB_CONNECTION_STRING")]) + end +end diff --git a/lib/pico_api/generators/templates/config.ru.erb b/lib/pico_api/generators/templates/config.ru.erb new file mode 100644 index 0000000..9de731f --- /dev/null +++ b/lib/pico_api/generators/templates/config.ru.erb @@ -0,0 +1,3 @@ +require './config/boot' + +run <%= @project_name_camelcased %>::Application.freeze.app diff --git a/lib/pico_api/generators/templates/config/application.erb b/lib/pico_api/generators/templates/config/application.erb new file mode 100644 index 0000000..ed552c9 --- /dev/null +++ b/lib/pico_api/generators/templates/config/application.erb @@ -0,0 +1,5 @@ +module <%= @project_name_camelcased %> + class Application < PicoApi::Application + # Add routing instructions here. https://github.com/jeremyevans/roda + end +end diff --git a/lib/pico_api/generators/templates/config/boot.erb b/lib/pico_api/generators/templates/config/boot.erb new file mode 100644 index 0000000..13d8438 --- /dev/null +++ b/lib/pico_api/generators/templates/config/boot.erb @@ -0,0 +1,9 @@ +require "pico_api" +require "./config/dotenv" if ENV['PICO_API_ENV'] == "development" +require "./config/configuration" +require "./config/database_setup" +require "./config/application" + +loader = Zeitwerk::Loader.new +loader.push_dir("./lib") +loader.setup diff --git a/lib/pico_api/generators/templates/config/configuration.erb b/lib/pico_api/generators/templates/config/configuration.erb new file mode 100644 index 0000000..d576d68 --- /dev/null +++ b/lib/pico_api/generators/templates/config/configuration.erb @@ -0,0 +1,13 @@ +PicoApi.configure do |config| + ## the base namespace of your app + config.namespace = "<%= @project_name_camelcased %>" + + ## the root directory where all the business logic resides + config.lib_path = "lib/<%= @project_name_snakecased %>" + + ## Add your custom exception mappings -> { exception: status_code }. + # PicoApi serializes runtime exceptions to json-api specification error objects + config.errors_map = { + StandardError: :unprocessable_entity + }.freeze +end diff --git a/lib/pico_api/generators/templates/config/database.yml b/lib/pico_api/generators/templates/config/database.yml new file mode 100644 index 0000000..afd4dc2 --- /dev/null +++ b/lib/pico_api/generators/templates/config/database.yml @@ -0,0 +1,8 @@ +# https://rom-rb.org/5.0/learn/sql/#general-connection-options + +default: + adapter: + connection_string: + options: + encoding: + diff --git a/lib/pico_api/generators/templates/config/database_setup.erb b/lib/pico_api/generators/templates/config/database_setup.erb new file mode 100644 index 0000000..82fd4ae --- /dev/null +++ b/lib/pico_api/generators/templates/config/database_setup.erb @@ -0,0 +1,6 @@ +PicoApi::Database.setup! do |config| + config.auto_registration( + File.expand_path(PicoApi.configuration.lib_path, './'), + namespace: PicoApi.configuration.namespace + ) +end diff --git a/lib/pico_api/generators/templates/config/dotenv.erb b/lib/pico_api/generators/templates/config/dotenv.erb new file mode 100644 index 0000000..763dce2 --- /dev/null +++ b/lib/pico_api/generators/templates/config/dotenv.erb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +unless ENV["RACK_ENV"] == "production" + require "dotenv" + Dotenv.overload ".env", ".env.local", ".env.#{ENV.fetch('RACK_ENV', nil)}" +end diff --git a/lib/pico_api/handlers/errors.rb b/lib/pico_api/handlers/errors.rb new file mode 100644 index 0000000..37bc641 --- /dev/null +++ b/lib/pico_api/handlers/errors.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module PicoApi + module Handlers + class Errors + def self.call(error, response) + exception_key = error.class.name.demodulize.to_sym + status = PicoApi.configuration.errors_map[exception_key] + return system_error(error, response) unless status + + response.status = status + Entities::Errors.call(error).to_h + end + + ServerError = Struct.new(:message) + def self.system_error(error, response) + response.status = :internal_server_error + error = ServerError.new('Something went wrong, we are investigating') + Entities::Errors.call(error).to_h + end + end + end +end diff --git a/lib/pico_api/json_api_renderer.rb b/lib/pico_api/json_api_renderer.rb new file mode 100644 index 0000000..8d8fe41 --- /dev/null +++ b/lib/pico_api/json_api_renderer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module PicoApi + module JsonApiRenderer + def serialize(resource, **opts) + serializer_klass = opts.delete(:serializer) + serializer = serializer_klass.new(resource, opts) + serializer.serializable_hash + end + end +end diff --git a/lib/pico_api/serializers/base.rb b/lib/pico_api/serializers/base.rb new file mode 100644 index 0000000..da38046 --- /dev/null +++ b/lib/pico_api/serializers/base.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'jsonapi/serializer' + +module PicoApi + module Serializers + class Base + include JSONAPI::Serializer + end + end +end diff --git a/lib/pico_api/version.rb b/lib/pico_api/version.rb new file mode 100644 index 0000000..91f8cc6 --- /dev/null +++ b/lib/pico_api/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module PicoApi + VERSION = '0.0.1' +end diff --git a/pico_api.gemspec b/pico_api.gemspec new file mode 100644 index 0000000..6579cd8 --- /dev/null +++ b/pico_api.gemspec @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative 'lib/pico_api/version' + +Gem::Specification.new do |spec| + spec.name = 'pico_api' + spec.version = PicoApi::VERSION + spec.authors = ['Alex Avlonitis'] + + spec.summary = 'A tiny Rack-based ruby template for APIs' + spec.description = 'A tiny Rack-based ruby template for APIs' + spec.homepage = 'https://github.com/alexavlonitis/pico_api' + spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0') + + spec.metadata['homepage_uri'] = spec.homepage + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.require_paths = ['lib'] + spec.executables << 'pico_api' + + spec.add_runtime_dependency 'erb', '~> 2.2.3' + spec.add_runtime_dependency 'jsonapi-serializer', '~> 2.2.0' + spec.add_runtime_dependency 'rackup', '~> 0.2.2' + spec.add_runtime_dependency 'rake', '~> 13.0.6' + spec.add_runtime_dependency 'roda', '~> 3.61.0' + spec.add_runtime_dependency 'rom', '~> 5.2.6' + spec.add_runtime_dependency 'rom-sql', '~> 3.5.0' + spec.add_runtime_dependency 'zeitwerk', '~> 2.6.1' +end