pragmarb / pragma Goto Github PK
View Code? Open in Web Editor NEWAn expressive, opinionated ecosystem for building beautiful RESTful APIs with Ruby.
Home Page: https://pragmarb.org
License: MIT License
An expressive, opinionated ecosystem for building beautiful RESTful APIs with Ruby.
Home Page: https://pragmarb.org
License: MIT License
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
This should probably be in Contract.
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.
# Responds and returns true
respond_with :not_found, entity: {}, headers: {}, decorator: MyDecorator
# Responds and returns false
respond_with! :not_found, entity: {}, headers: {}, decorator: MyDecorator
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.
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
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.
Looking to move to pragma, but would love to see some examples of tests and documentation.
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
.
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 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)
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:
Pragma::ORM::AssociationEagerloading
(from pragma)Pragma::ORM::AssociationEmbedding
(from pragma-decorator)Pragma::ORM::Pagination
(from pragma-decorator)Pragma::ORM::Finder
(from pragma-contract)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.
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.
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.
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]
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.
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
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
.
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.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.