GithubHelp home page GithubHelp logo

pragmarb / pragma Goto Github PK

View Code? Open in Web Editor NEW
92.0 4.0 3.0 343 KB

An expressive, opinionated ecosystem for building beautiful RESTful APIs with Ruby.

Home Page: https://pragmarb.org

License: MIT License

Ruby 96.78% Shell 3.22%
ruby ruby-on-rails trailblazer api ecosystem pragma

pragma's People

Contributors

aldesantis avatar ediogodias avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

pragma's Issues

Tracking updated attributes

class Update < Pragma::Operation::Update
  step :do_something!

  def do_something!(options)
    # { "my_attr" => ["before_value", "after_value"] }
    return unless options['result.updated_attributes'].key?('my_attr')

    # do something...
  end
end

Support IdentityCache

We should support caching, but it should be opt-in, e.g.

class Show < Pragma::Operation::Show
  self['model.caching'] = true
end

This should use #fetch instead of #find_by in Model (and fail when #fetch is not defined). It should also use #fetch_[assoc] for associations in the decorator adapter (when defined).

Perhaps it should be its own gem, e.g. Pragma::Cache.

Simplify responding

# Responds and returns true
respond_with :not_found, entity: {}, headers: {}, decorator: MyDecorator

# Responds and returns false
respond_with! :not_found, entity: {}, headers: {}, decorator: MyDecorator

Auto-include expanded associations

Pragma::Operation::Index.prepend (Module.new do
  def find_records
    relation = super
    if RolloutWrapper.active?(:auto_include_associations)
      relation = auto_include_associations(relation, params[:expand]) if params[:expand].present?
    end
    relation
  end

  private

  def auto_include_associations(relation, associations)
    to_include = associations.each_with_object({}) do |association, hash|
      hash = destruct_association(association, hash)
    end
    to_include = validate_associations(relation.model, to_include)
    Rails.logger.debug "Including: #{to_include}"
    relation.includes(to_include)
  end

  def destruct_association(association, hash = {})
    split = association.split('.')
    if split.length > 1
      value = split.shift
      hash[value] = {} unless hash[value]
      destruct_association(split.join('.'), hash[value])
    else
      value = split.shift
      hash[value] = {} unless hash[value]
    end
    hash
  end

  def validate_associations(model, associations)
    return associations if associations.empty?

    associations.each_with_object({}) do |(key, value), object|
      reflection = model.reflect_on_association(key.to_sym)
      if reflection.present?
        object[key] = validate_associations(reflection.klass, value)
      else
        {}
      end
    end
  end
end)

Courtesy of @ediogodias.

Pragma::Metadata

This would be a gem to associate metadata with any API resources, like Stripe does in their API.

Usage should be pretty simple.

# app/models/post.rb
class Post < ApplicationRecord
  include Pragma::Metadata::Model
  # ...
end

# app/resources/api/v1/post/contract/base.rb
class API::V1::Post::Contract::Base < Pragma::Contract::Base
  include Pragma::Metadata::Contract
  # ...
end

# app/resources/api/v1/post/decorator/instance.rb
class API::V1::Post::Decorator::Instance < Pragma::Decorator::Base
  include Pragma::Metadata::Decorator
  # ...
end

Resource definition DSL

Rather than requiring users to write/generate boilerplate code for each resource, it shouldn't be too hard to allow users to define resources with a DSL and provide hooks for customization, e.g.

# app/resources/api/v1/post.rb
Pragma::Resource.define :post do |config| # entire block is optional
  config.model_class = ::Post # optional, computed by default

  config.attributes do |attributes|
    attributes.define :title, :body do
      type :string
      validate :required?
    end

    attributes.define :author do
      default ->(options) { options['current_user'] }
      validate :required?
      visible false # not exposed in decorator
    end

    attributes.define :send_newsletter do
      type :boolean
      only :create # cannot be updated
      virtual # not saved to model
    end
  end

  # These would accept any callable object which receives `options`.
  # We would also have before_* and around_* hooks.
  config.hooks do |hooks|
    hooks.after_create API::V1::Post::Hook::NotifySubscribers
    hooks.after_update API::V1::Post::Hook::TouchLastUpdate
    hooks.after_save API::V1::Post::Hook::GenerateSummary
    hooks.after_destroy API::V1::Post::Hook::RemoveFromFeed
  end

  config.policy do |policy|
    policy.on :create? do |user, post|
      # ...
    end
  end
end

We should still support custom resources defined the old way.

This would probably be a separate library/gem (Pragma::Resource?) providing a catch-all API endpoint that looks at the configuration and executes the appropriate logic.

Automatic ordering

class Index < Pragma::Operation::Index
  self['ordering.columns'] = %i[created_at title]
  self['ordering.default_column'] = :created_at
  self['ordering.default_direction'] = :desc
end

Also, order by created_at DESC by default, if the model responds to created_at.

Policy(): Support overriding action name

It should be possible to do this:

class CustomCreate < Create
  step Pragma::Operation::Macro::Policy(action: :create?)
end

So that this is not required anymore:

class Policy < Pragma::Policy::Base
  def custom_create?
    create?
  end
end

Using with `pragma-devise`, error `uninitialized constant API::V1::Token::Contract`

Using pragma 1.2.4 with pragma-devise kept returning an uninitialized constant API::V1::Token::Contract error.

This seems to be stemming from const_get at line 73 of pragma-1.2.4/lib/pragma/operation/defaults.rb

        def class_exists?(klass)
          begin
            Object.const_get(klass)
          rescue NameError => e
            raise e unless e.message.include?("uninitialized constant #{klass}")
          end

          Object.const_defined?(klass)
        end

Downgrading to 1.2.1 fixed the issue (temporarily)

Extract ORM-specific logic in pragma-orm

We're starting to have a lot of ORM-dependent logic (association loading, association inclusion, record finding and pagination). Right now this logic is spread across pragma, pragma-contract and pragma-decorator, which means adding support for a new ORM requires opening PRs in three separate gems (possibly more in the future as we add more features).

Perhaps it would be a good idea to extract all ORM-dependent logic into a central pragma-orm gem that is then required in the public gems. I envision the following modules:

The downside is that the gems would bundle all ORM support features even when not all of them are used, but this seems like an acceptable tradeoff.

Another problem could be that currently the ORM support modules are tightly coupled to the gems they're part of. The most notable example of this is Pragma::Decorator::Association which performs a lot of checks on the consistency between user data and the real association. This could be solved with some sort of abstraction but it needs to be designed properly.

Schema() macro for headless validation

https://github.com/pragmarb/pragma/wiki/Validating-query-parameters

If we implemented a Schema() macro in all operations that checks whether a schema.default skill is present and runs validations, query parameter validation could be simplified as follows:

module API
  module V1
    module Article
      module Operation
        class Index < Pragma::Operation::Index
          self['schema.default'] = Dry::Validation.Schema do
            optional(:user_id).maybe(:int?)
          end
        end
      end
    end
  end
end

This would also be very useful for headless operations, since they wouldn't require a model anymore.

Pragma::Auth

This would be a gem for authentication that provides an API endpoint to generate JWT tokens and a macro to authenticate users.

In addition to DRYing up authentication logic, it would allow us to decouple authentication from Rails by not requiring the definition of a current_user method.

Not sure if this should come in the form of a Rails engine or just operation classes that are then extended by the main app.

Model(): Support lookup by custom attributes

We should support the following formats:

step Model(invoice_id: :id) # WHERE invoice_id = params[:invoice_id] AND id = params[:id]
step Model(:id) # WHERE id = params[:id]
step Model(:slug, :id) # WHERE slug = params[:id] OR id = params[:id]

HATEOAS support

  • In Pragma::Decorator::Pagination operation, include links to the other pages.
  • Create a module for decorators to include a link to the resource itself.

Classes() does not support nested operations

It shouldn't be required to explicitly specify classes when the operation is nested, e.g.

class Create < Pragma::Operation::Create
  class CustomCreate < Create
    step Pragma::Operation::Macro::Classes()
  end
end

Currently, this will fail to locate all the classes, because it looks in the wrong namespace.

Interestingly enough, this works and is just plain Ruby:

self['model.class'] = '???' # not sure how to do this one
self['policy.default.class'] = Policy
self['policy.default.scope.class'] = Policy::Scope
self['decorator.instance.class'] = Decorator::Instance
self['decorator.collection.class'] = Decorator::Collection
self['contract.default.class'] = Contract::Create::CustomCreate

This can be done by computing namespace via string matching rather than indexing, e.g.

operation_klass = 'API::V1::Post::Operation::Create::CustomCreate'
contract_klass = operation_klass.

Automatic filtering

class Index < Pragma::Operation::Index
  self['filters'] = [
    Pragma::Operation::Filter::Like.new(param: :by_name, column: :name),
    Pragma::Operation::Filter::Ilike.new(param: :by_iname, column: :name),
    Pragma::Operation::Filter::Eq.new(param: :by_country_code, column: :country_code),
  ]
end

Indicate where an operation has halted

It would be nice to have a way to see what was the last step executed by an operation, so that we can easily understand what is halting an operation.

We could set the last executed step on the operation's skill, e.g.

result = Api::V2::User::Operation::Show.call('id' => 1)
result['result.last_step'] # => "model.find_by"
result['result.failing_step'] # => "policy.default"

result.failing_step here can be computed like this (here be dragons):

steps = Api::V2::User::Operation::Show.skills['pipetree'].instance_variable_get('@index').keys
steps[steps.index(result['result.last_step']) + 1]

This should also be indicated in Pragma::Rails::NoResponseError.

Normalize pipetrees

Right now, a lot of the pipetrees are using a mix of symbols and strings - also, some steps have an exclamation mark at the end, while others do not.

We should override the name of all steps to use strings without an exclamation mark.

Decorate errors automatically

Related to pragmarb/pragma-operation#2.

This pattern is very annoying:

options['result.response'] = Pragma::Operation::Response::NotFound
  .new
  .decorate_with(Pragma::Decorator::Error)

There is no reason for that decorate_with to exist, it's just boilerplate.

If result.response is a Pragma error response and it's not decorated, we should decorate it with Pragma::Decorator::Error automatically.

We either do it in pragma-operation or decorate the base operation here, e.g.

class Pragma::Operation::Base < Trailblazer::Operation
  def call(*)
    result = super
    result['result.response'] = '...'
  end
end

The problem with this is it cannot be overridden since it's not a step, and there is no way to ensure a step is actually run at the end of the operation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.