├── .drone.yml ├── LICENSE ├── README.md ├── shard.yml ├── spec └── object_send_spec.cr └── src └── object_send.cr /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | platform: 5 | os: linux 6 | arch: amd64 7 | 8 | steps: 9 | - name: format 10 | image: jrei/crystal-alpine 11 | commands: 12 | - crystal tool format --check 13 | 14 | - name: test 15 | image: jrei/crystal-alpine 16 | commands: 17 | - crystal spec 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019 Julien Reichardt 4 | 5 | Permission to use, copy, modify, and distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crystal Object#send 2 | 3 | [![Build Status](https://cloud.drone.io/api/badges/j8r/crystal-object-send/status.svg)](https://cloud.drone.io/j8r/crystal-object-send) 4 | [![ISC](https://img.shields.io/badge/License-ISC-blue.svg?style=flat-square)](https://en.wikipedia.org/wiki/ISC_license) 5 | 6 | Interpret a String to an Object method call. 7 | 8 | Similar to the Ruby's `Object#send`. 9 | 10 | Here macros and the `Crystal::Parser` are used to build a pseudo interpreter. 11 | 12 | ## Disclaimer 13 | 14 | There are lots of limitations, this library is mainly an experiment. 15 | 16 | Usually you better avoid using it and interpret the string on your own. It would be more performant and safer. 17 | 18 | ## Installation 19 | 20 | Add the dependency to your `shard.yml`: 21 | 22 | ```yaml 23 | dependencies: 24 | object-send: 25 | github: j8r/crystal-object-send 26 | ``` 27 | 28 | ## Examples 29 | 30 | ```cr 31 | "abc".send "chars" #=> ['a', 'b', 'c'] 32 | "abc".send "lchop('a')" #=> "bc" 33 | "abc".send "insert 1, 'z'" #=> "azbc" 34 | 2.send("+ 3") #=> 5 35 | 36 | var = "first 2" 37 | [0, 1, 3].send(var) #=> [0, 1] 38 | [0, 1, 2].send("[-1]?") #=> eq 2 39 | [0, 1, 2].send("[..]") #=> eq [0, 1, 2] 40 | ``` 41 | 42 | See more in the [specs](spec/object_send_spec.cr) 43 | 44 | ## License 45 | 46 | Copyright (c) 2019 Julien Reichardt - ISC License 47 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: object-send 2 | version: 0.3.1 3 | 4 | authors: 5 | - Julien Reichardt 6 | 7 | description: Interpret a String to an Object method call 8 | 9 | license: ISC 10 | -------------------------------------------------------------------------------- /spec/object_send_spec.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/object_send" 3 | 4 | describe "send" do 5 | describe Array do 6 | it "gets index with []?" do 7 | [0, 1, 2].send("[1]?").should eq 1 8 | end 9 | 10 | it "gets range with []" do 11 | [0, 1, 2].send("[..]").should eq [0, 1, 2] 12 | end 13 | 14 | it "first" do 15 | [0, 1, 2].send("first 2").should eq [0, 1] 16 | end 17 | 18 | it "deletes at Int index, Int count" do 19 | [0, 1, 2].send("delete_at(1, 1)").should eq [1] 20 | end 21 | 22 | it "deletes within a Range" do 23 | ary = [0, 1, 2] 24 | ary.send("delete_at(1..2)").should eq [1, 2] 25 | ary.should eq [0] 26 | end 27 | end 28 | 29 | describe Char do 30 | it "ord" do 31 | 'a'.send("ord").should eq 97 32 | end 33 | end 34 | 35 | describe Float64 do 36 | it "divide with /" do 37 | 3.2.send("/ 2").should eq 1.6 38 | end 39 | end 40 | 41 | describe Hash do 42 | it "keys" do 43 | {"a" => 'b'}.send("keys").should eq ["a"] 44 | end 45 | end 46 | 47 | describe Int32 do 48 | it "+ Int32" do 49 | 2.send("+ 3").should eq 5 50 | end 51 | 52 | it "==" do 53 | 2.send("== 2").should be_true 54 | end 55 | end 56 | 57 | describe IO do 58 | it "puts" do 59 | io = IO::Memory.new 60 | io.send(%(puts "abc")) 61 | io.to_s.should eq "abc\n" 62 | end 63 | end 64 | 65 | describe String do 66 | it "chars" do 67 | "abc".send("chars").should eq ['a', 'b', 'c'] 68 | end 69 | 70 | it "size" do 71 | var = "size" 72 | "abc".send(var).should eq 3 73 | end 74 | 75 | it "size()" do 76 | "abc".send("size()").should eq 3 77 | end 78 | 79 | it "starts_with? Char" do 80 | "abc".send("starts_with? 'a'").should be_true 81 | end 82 | 83 | it "lchop(Char)" do 84 | "abc".send("lchop('a')").should eq "bc" 85 | end 86 | 87 | it "insert Index, Char" do 88 | "abc".send("insert 1, 'z'").should eq "azbc" 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /src/object_send.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/syntax" 2 | 3 | class Object 4 | private def cast_node(node : Crystal::ASTNode) 5 | case node 6 | when Crystal::StringLiteral then node.value 7 | when Crystal::CharLiteral then node.value 8 | when Crystal::BoolLiteral then node.value 9 | when Crystal::NilLiteral, Crystal::Nop then nil 10 | when Crystal::NumberLiteral 11 | case node.kind 12 | when :f32 then node.value.to_f32 13 | when :f64 then node.value.to_f64 14 | else node.integer_value 15 | end 16 | when Crystal::RangeLiteral 17 | Range.new( 18 | cast_node(node.from).as(Int::Primitive?), 19 | cast_node(node.to).as(Int::Primitive?), 20 | node.exclusive? 21 | ) 22 | else 23 | raise "unsupported node type: #{node} (#{node.class}}" 24 | end 25 | end 26 | 27 | def send(call : String) 28 | args = nil 29 | name = nil 30 | node = Crystal::Parser.parse "self." + call 31 | 32 | {% begin %} 33 | {% args = %w(0 1 2 3 4 5 6 7 8 9) %} 34 | {% supported_types = %w(Int Bool Char Float32 Float64 Int16 Int32 Int64 Int8 String UInt16 UInt32 UInt64 UInt8 Nil Range) %} 35 | 36 | case node 37 | when Crystal::Call 38 | name = node.name 39 | args = node.args 40 | raise "max number of arguments reached: {{args.last.id}}" if args.size > {{args.last.id}} 41 | method_with_args = case args.size 42 | {% for arg_num in args %}\ 43 | when {{arg_num.id}} 44 | { name {% for local_arg_num in args %}{% if local_arg_num < arg_num %}, cast_node(args[{{local_arg_num.id}}]){% end %}{% end %} } 45 | {% end %} 46 | end 47 | end 48 | 49 | case method_with_args 50 | when nil then raise "unsupported call: #{call}" 51 | {% methods = @type.methods %}\ 52 | {% for type in @type.ancestors %}\ 53 | {% methods = methods + type.methods %}\ 54 | {% end %}\ 55 | {% used_methods = {} of String => Bool? %}\ 56 | {% for method in methods %} 57 | # {{method.name}} {{method.args}} 58 | {% if method.accepts_block? || 59 | used_methods[method.name.stringify + method.args.map(&.restriction.stringify).join("")] || 60 | %w(sum transpose product to_h).includes?(method.name.stringify) %}\ 61 | {% elsif method.args.all? &.restriction.stringify.split(" | ").all? { |t| supported_types.includes? t } %}\ 62 | {% method_args = "" %}\ 63 | when { {{method.name.stringify}} {% for arg in method.args %}\ 64 | , {{arg.restriction}}\ 65 | {% method_args = method_args + arg.restriction.stringify %}\ 66 | {% end %}\ } 67 | {% i = 0 %}\ 68 | self.{{method.name}}( 69 | {% for arg in method.args %}\ 70 | {% i = i + 1 %}\ 71 | method_with_args[{{i.id}}]?.as({{arg.restriction}}){% if method.args.size > 1 %},{% end %}\ 72 | {% end %}\ 73 | ) 74 | {% used_methods[method.name.stringify + method_args] = true %}\ 75 | {% end %}\ 76 | {% end %}\ 77 | else 78 | raise "unsupported method: #{method_with_args}" 79 | end 80 | {% end %} 81 | end 82 | end 83 | --------------------------------------------------------------------------------