-
-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add pretty print for policy rules
- Loading branch information
Showing
8 changed files
with
270 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
source "https://rubygems.org" | ||
|
||
gem "sqlite3" | ||
gem "rails", "6.0.0.beta2" | ||
gem "rails", "6.0.0.beta3" | ||
gem "method_source" | ||
gem "unparser" | ||
|
||
gemspec path: ".." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# frozen_string_literal: true | ||
|
||
begin | ||
require "method_source" | ||
require "parser/current" | ||
require "unparser" | ||
rescue LoadError | ||
# do nothing | ||
end | ||
|
||
module ActionPolicy | ||
require "action_policy/ext/yield_self_then" | ||
using ActionPolicy::Ext::YieldSelfThen | ||
|
||
# Takes the object and a method name, | ||
# and returns the "annotated" source code for the method: | ||
# code is split into parts by logical operators and each | ||
# part is evaluated separately. | ||
# | ||
# Example: | ||
# | ||
# class MyClass | ||
# def access? | ||
# admin? && access_feed? | ||
# end | ||
# end | ||
# | ||
# puts PrettyPrint.format_method(MyClass.new, :access?) | ||
# | ||
# #=> MyClass#access? | ||
# #=> ↳ admin? #=> false | ||
# #=> AND | ||
# #=> access_feed? #=> true | ||
module PrettyPrint | ||
class Visitor | ||
attr_reader :lines, :object | ||
attr_accessor :indent | ||
|
||
def initialize(object) | ||
@object = object | ||
end | ||
|
||
def collect(ast) | ||
@lines = [] | ||
@indent = 0 | ||
|
||
visit_node(ast) | ||
|
||
lines.join("\n") | ||
end | ||
|
||
def visit_node(ast) | ||
if respond_to?("visit_#{ast.type}") | ||
send("visit_#{ast.type}", ast) | ||
else | ||
visit_missing ast | ||
end | ||
end | ||
|
||
def expression_with_result(sexp) | ||
expression = Unparser.unparse(sexp) | ||
"#{expression} #=> #{eval_exp(expression)}" | ||
end | ||
|
||
def eval_exp(exp) | ||
object.instance_eval(exp) | ||
rescue => e | ||
"Failed: #{e.message}" | ||
end | ||
|
||
def visit_and(ast) | ||
visit_node(ast.children[0]) | ||
lines << indented("AND") | ||
visit_node(ast.children[1]) | ||
end | ||
|
||
def visit_or(ast) | ||
visit_node(ast.children[0]) | ||
lines << indented("OR") | ||
visit_node(ast.children[1]) | ||
end | ||
|
||
# Parens | ||
def visit_begin(ast) | ||
lines << indented("(") | ||
self.indent += 2 | ||
visit_node(ast.children[0]) | ||
self.indent -= 2 | ||
lines << indented(")") | ||
end | ||
|
||
def visit_missing(ast) | ||
lines << indented(expression_with_result(ast)) | ||
end | ||
|
||
def indented(str) | ||
"#{indent.zero? ? "↳ " : ""}#{" " * indent}#{str}".tap do | ||
# increase indent after the first expression | ||
self.indent += 2 if indent.zero? | ||
end | ||
end | ||
end | ||
|
||
class << self | ||
if defined?(::Unparser) && defined?(::MethodSource) | ||
def available? | ||
true | ||
end | ||
|
||
def print_method(object, method_name) | ||
ast = object.method(method_name).source.then(&Unparser.method(:parse)) | ||
# outer node is a method definition itself | ||
body = ast.children[2] | ||
|
||
Visitor.new(object).collect(body) | ||
end | ||
else | ||
def available? | ||
false | ||
end | ||
|
||
def print_method(_, _) | ||
"" | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# frozen_string_literal: true | ||
|
||
require "test_helper" | ||
|
||
class PrettyPrintPolicy < ActionPolicy::Base | ||
def feed? | ||
(admin? || allowed_to?(:access_feed?)) && | ||
(user.name == "Jack" || user.name == "Kate") | ||
end | ||
|
||
def edit? | ||
(user.name == "John") && (admin? || access_feed?) | ||
end | ||
|
||
def access? | ||
admin? && access_feed? | ||
end | ||
|
||
def admin? | ||
user.admin? | ||
end | ||
|
||
def access_feed? | ||
true | ||
end | ||
end | ||
|
||
class TestPrettyPrint < Minitest::Test | ||
def setup | ||
skip unless ActionPolicy::PrettyPrint.available? | ||
@user = User.new("Kate") | ||
@policy = PrettyPrintPolicy.new(user: @user) | ||
end | ||
|
||
attr_reader :policy | ||
|
||
def test_single_expression_rule | ||
expected = <<~EXPECTED | ||
PrettyPrintPolicy#admin? | ||
↳ user.admin? #=> false | ||
EXPECTED | ||
|
||
assert_output(expected) { policy.pp(:admin?) } | ||
end | ||
|
||
def test_multi_expression_rule | ||
expected = <<~EXPECTED | ||
PrettyPrintPolicy#access? | ||
↳ admin? #=> false | ||
AND | ||
access_feed? #=> true | ||
EXPECTED | ||
|
||
assert_output(expected) { policy.pp(:access?) } | ||
end | ||
|
||
def test_multi_expression_with_parentheses | ||
expected = <<~EXPECTED | ||
PrettyPrintPolicy#edit? | ||
↳ ( | ||
user.name == "John" #=> false | ||
) | ||
AND | ||
( | ||
admin? #=> false | ||
OR | ||
access_feed? #=> true | ||
) | ||
EXPECTED | ||
|
||
assert_output(expected) { policy.pp(:edit?) } | ||
end | ||
|
||
def test_multi_parentheses | ||
expected = <<~EXPECTED | ||
PrettyPrintPolicy#feed? | ||
↳ ( | ||
admin? #=> false | ||
OR | ||
allowed_to?(:access_feed?) #=> true | ||
) | ||
AND | ||
( | ||
user.name == "Jack" #=> false | ||
OR | ||
user.name == "Kate" #=> true | ||
) | ||
EXPECTED | ||
|
||
assert_output(expected) { policy.pp(:feed?) } | ||
end | ||
end |