Reusable Custom DSL Extension in Grape - ruby

I have a custom Grape DSL method called scope that remembers the parameter then delegates some work to the params helper.
class API::V1::Walruses < Grape::API
resources :walruses do
scope :with_tusks, type: Integer,
desc: "Return walruses with the exact number of tusks"
scope :by_name, type: String,
desc: "Return walruses whose name matches the supplied parameter"
get do
# Equivalent to `Walrus.with_tusks(params[:with_tusks]).
# by_name(params[:by_name])`, but only calls scopes whose
# corresponding param was passed in.
apply_scopes(Walrus)
end
end
end
This works and is clean, which I like. The problem is that many child endpoints also want to use those same scopes. This means maintaining them in multiple places and keeping them all in sync.
I would like to do the following instead:
class API::V1::Walruses < Grape::API
helpers do
scopes :index do
scope :with_tusks, type: Integer,
desc: "Return walruses with the exact number of tusks"
scope :by_name, type: String,
desc: "Return walruses whose name matches the supplied parameter"
end
end
resources :walruses do
use_scopes :index
#...
end
end
Then API::V1::Walruses will have those scopes available, as will API::V1::Walruses::Analytics (if included), and any others that are nested within.
Params have a similar methodology but I've had trouble understanding how to inherit settings.

Related

Authorization on a field in graphql-ruby

From this guide I don't understand how we can add authorization on a field in graphql-ruby.
I understand how we can deny access to an entire object but I don't see how we can prevent access to just a field of an object.
In my usecase, I want to prevent some queries to access a String field in my UserType.
Is it possible? With the field helper?
You could use scoping in the GraphQL-ruby or use gem graphql-guard.
For scoping to work, you should be either using Pundit scope or CanCan accessible_by.
FYI, I haven't used it yet anywhere but seems like both could solve your problem to me.
I used gem graphql-guard.
GUARD = lambda do |obj, args, ctx|
...some logics...
end
field :some_field, String, null: false, guard: GUARD
And when I wanted to use only some of the arguments like lambda do |_, args, _|
Here's an example:
module Types
class User < Types::BaseObject
field :private_string, String, null: true do
def authorized?(object, args, context)
# Do whatever logic you want and return true if the user is authorized.
object.id == context[:current_user].id
end
end
end
end
class MySchema < GraphQL::Schema
# You'll need this in your schema to handle an unauthorized field.
def self.unauthorized_field(error)
raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions"
end
end

Make a route_param embedded into a resource a string

I am trying to make a route parameter embedded inside a resource as a string.
class Example < Grape::API
resource 'a/:param/b' do
# code
end
end
One way I could think of is by splitting the resource into route_param blocks and make it a string but this will introduce a lot of nesting which would defeat the purpose of the resource having an embedded route_param and thereby preventing the nesting.
class Example < Grape::API
resource 'a' do
route_param :param, type: String do
resource 'b' do
# code
end
end
end
end
Another way I could think of is by explicitly defining in a param block which would potentially duplicate a lot of code.
class Example < Grape::API
resource 'a/:param/b' do
params do
requires :param,
type: String
end
# code
end
end
Is anybody aware of a different way? Thanks!

Change class instance variable from a module

I have several classes (Elves, Dwarves, Giants, etc.), and I would like to add "powers" to them via a powerable module that lets me add several fields/methods to a model (I am using Mongoid just for the sake of example) using a simple DSL has_superpower(power_name)
module Powerable::Elf
extend ActiveSupport::Concern
include Powerable::Mongoid
has_superpower(:super_hearing)
has_superpower(:night_vision)
end
class Elf
include Powerable::Elf
end
Each time I call the class method has_superpower(power_name), it should register the power_name somewhere so I can later get the list of all added powers on a class (and reflect on those names to call other methods).
I don't know how to do that. I had a look at class instance variables or class variables but I'm not sure how to use them for this context
Consider the following boilerplate
module Powerable
module Mongoid
extend ActiveSupport::Concern
def use_power!(power_name, options = {})
send(:"last_#{power_name}_used_at=", Time.now)
...
# Log the power use to the :#{power_name}_uses array
push(#{power_name}_uses, power: power_name, time: Time.now, **options)
end
class_methods do
def has_superpower(power_name)
field :"last_#{power_name}_used_at", type: DateTime
field :"last_#{power_name}_count", type: Integer
field :"#{power_name}_uses", type: Array
end
end
end
Now I would like to define a method that gives me all the uses of all superpowers. For this I would need to register the powers one has, so I can resolve the model fields I need to look at
# I am adding the following to the above module Powerable::Mongoid
module Powerable
module Mongoid
def power_uses(power_name = nil)
if power_name.blank? # Retrieve all power uses)
self.class.all_powers.flat_map do |power_name|# 👀 I do not know how to code all_powers
send(:"#{power_name}_uses")
end
else
send(:"#{power_name}_uses")
end
end
end
end
The idea
elf = Elf.new
elf.use_power!(:night_vision, moon: :crescent)
elf.use_power!(:super_hearing, distance: 1.kilometer)
elf.power_uses # => [
power: night_vision, time: xxx, moon: :crescent,
power: super_hearing, time: xxx, distance: 1.kilometer
]

When creating a custom Jekyll tag, what is the `tokens` arg used for?

The Jekyll docs say we can create a custom tag:
module Jekyll
class TestTag < Liquid::Tag
def initialize(tag_name, text, tokens)
super
#text = text
#tokens = tokens
end
def render(context)
"text: #{#text} tokens: #{#tokens}"
end
end
end
Liquid::Template.register_tag('test', Jekyll::TestTag)
It seems like initialize is a built-in function, although the docs don't explicitly say that.
When I include this tag in a page:
{% test hallo world %}
I get:
text: hallo world tokens: {:locale=>#<Liquid::I18n:0x007fd62dbd5e38
#path=”/Library/Ruby/Gems/2.0.0/gems/liquid-3.0.6/lib/liquid/locales/en.yml”>,
:line_numbers=>true}
Where are these tokens coming from? What do they do? Can I set tokens myself?
Where are these tokens coming from?
You are using the super keyword, that means it calls the initialize method of its parent class, in this case Liquid::Tag, it is the constructor of the class and creates a new instance of Tag.
What do they do?
The tokens argument:
is a hash that stores Liquid options. By default it
has two keys: :locale and :line_numbers, the first is a Liquid::I18n
object, and the second, a boolean parameter that determines if error
messages should display the line number the error occurred. This
argument is used mostly to display localized error messages on Liquid
built-in Tags and Filters.
when defining a method using the super keyword tells the parser to look for a method of the same name along the lookup path.
Liquid::Tag has an initialize method, and that is where those tokens are most likely coming from.

How to write regression tests for custom Chef resources?

Given the minimal example
# resources/novowel.rb
resource_name :novowel
property :name, String, name_property: true, regex: /\A[^aeiou]\z/
I would like to write the unit tests in spec/unit/resources/novowel_spec.rb
Resource 'novowel' for name should accept 'k'
Resource 'novowel' for name should accept '&'
Resource 'novowel' for name should NOT accept 'a'
Resource 'novowel' for name should NOT accept 'mm'
to ensure that the name property still works correctly even if the regex is changed for some reason.
I browsed several top notch Chef cookbooks but could not find references for such testing.
How can it be done? Feel free to provide more complex examples with explicitly subclassing Chef::Resource if that helps achieve the task.
Update 1: Could it be that Chef does NOT FAIL when a property does not fit the regex? Clearly this should not work:
link '/none' do
owner 'r<oo=t'
to '/usr'
end
but chef-apply (12.13.37) does not complain about r<oo=t not matching owner_valid_regex. It simply converges as if owner would not have been provided.
You would use ChefSpec and RSpec. I've got examples in all of my cookbooks (ex. https://github.com/poise/poise-python/tree/master/test/spec/resources) but I also use a bunch of custom helpers on top of plain ChefSpec so it might not be super helpful. Doing in-line recipe code blocks in the specs makes it waaaay easier. I've started extracting my helpers out for external use in https://github.com/poise/poise-spec but it's not finished. The current helpers are in my Halite gem, see the readme there for more info.
We wrap the DSL inside a little Ruby in order to know the name of the resource's Ruby class:
# libraries/no_vowel_resource.rb
require 'chef/resource'
class Chef
class Resource
class NoVowel < Chef::Resource
resource_name :novowel
property :letter, String, name_property: true, regex: /\A[^aeiou]\z/
property :author, String, regex: /\A[^aeiou]+\z/
end
end
end
and now we can use RSpec with
# spec/unit/libraries/no_vowel_resource_spec.rb
require 'spec_helper'
require_relative '../../../libraries/no_vowel_resource.rb'
describe Chef::Resource::NoVowel do
before(:each) do
#resource = described_class.new('k')
end
describe "property 'letter'" do
it "should accept the letter 'k'" do
#resource.letter = 'k'
expect(#resource.letter).to eq('k')
end
it "should accept the character '&'" do
#resource.letter = '&'
expect(#resource.letter).to eq('&')
end
it "should NOT accept the vowel 'a'" do
expect { #resource.letter = 'a' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
it "should NOT accept the word 'mm'" do
expect { #resource.letter = 'mm' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
end
describe "property 'author'" do
it "should accept a String without vowels" do
#resource.author = 'cdrngr'
expect(#resource.author).to eq('cdrngr')
end
it "should NOT accept a String with vowels" do
expect { #resource.author = 'coderanger' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
end
end

Resources