Escape the %r{...} literals in macro is necessary or not?

Following is reproduce:

  1. mkdir test && cd ./test
  2. mkdir -p spec/graphql && touch spec/spec_helper.cr && touch spec/graphql/hello_spec.cr
  3. Add following content into spec/spec_helper.cr
macro query(query_path = "")
  {% root = system("pwd").strip.id %}

  macro strip_path
    \{%
      if {{query_path}} != ""
        graphql_queries_path = "./spec/graphql_queries/{{query_path.id}}.graphql"
      else
        graphql_queries_path = __FILE__
                               .gsub(%r{{{root}}}, ".")
                               .gsub(\%r{graphql}, "graphql_queries")
                               .gsub(\%r{_spec\.cr}, ".graphql")
      end
    %}

      \{% if file_exists? graphql_queries_path %}
            \{{read_file(graphql_queries_path)}}
      \{% else %}
         raise "`\{{graphql_queries_path.id}}' not exists!"
      \{% end %}
     end

  p strip_path.chomp
end
  1. echo ‘require “…/spec_helper”; query’ > spec/graphql/hello_spec.cr

  2. Run crystal run spec/graphql/hello_spec.cr, it raise a exception as expected.

Unhandled exception: ./spec/graphql_queries/hello.graphql’ not exists! (Exception)`

Basically, i want to do the same thing as the caller hack in this answer, but use macro, at compile time.

Let us use above files as a example, when query is invoke, i want to know where it be invoke, in this cause, it is spec/graphql/hello_spec.cr, then, i transform this path to spec/graphql_queries/a/b/hello.graphql, then read file content from it.


Following is my questions:

Question 1

Why the % in 1 don’t need escape, but, 2,3 must escaped?

otherwise, replace the \%r into %r for 2, will report following error?

 ╰─ $ crystal run spec/graphql/hello_spec.cr 
In spec/spec_helper.cr:1:7

 1 | macro query(query_path = "")
           ^
Error: unterminated macro

BTW, if the the Regexp's pattern of 2 was not matched, e.g. . `%r{hello}`, not raise above error.

Anyway, i consider this maybe a bug of macro?

Question 2

In fact, this macro not work as expected.

we create the missing graphql file first.

  1. mkdir -p ./spec/graphql_queries
  2. echo “query { hello() {}}” > ./spec/graphql_queries/hello.graphql
  3. Run hello_spec.cr, it works as expected.

╰─ $ crystal run spec/graphql/hello_spec.cr
“query { hello() {}}”

  1. Then, change the content of spec/graphql/hello_spec.cr into ../spec_helper"; p query(add a p before query macro), get following error.
 ╰─ $ crystal run spec/graphql/hello_spec.cr 
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'query'

Code in spec/graphql/hello_spec.cr:1:29

 1 | require "../spec_helper"; p query
                                 ^
Called macro defined in spec/spec_helper.cr:1:1

 1 | macro query(query_path = "")

Which expanded to:

 > 1 |   
 > 2 | 
 > 3 |   macro strip_path
               ^---------
Error: can't declare macro dynamically

Question 3

Finally, I done this use a method, on the runtime (as ruby does)

def query(query_path="")
  if query_path != ""
    graphql_queries_path = "./spec/graphql_queries/#{query_path}.graphql"
  else
    graphql_queries_path = caller[2][/(.+?):\d+.*/, 1].gsub("graphql", "graphql_queries").gsub("_spec.cr", ".graphql")
  end

  if File.exists? graphql_queries_path
    File.read(graphql_queries_path)
  else
    raise "`File #{graphql_queries_path}' not exists!"
  end
end

But, i still consider we can do same thing use macro on the compile time, right?

Thank you.

I haven’t looked too far into this, but at first glance, is there a reason this needs to be a macro? You can do file path manipulation and reading at runtime. And given they’re specs the use of read_file is of little benefit.

Yes, i found my code not work by this error “Error: can’t declare macro dynamically” when the macro is expanding, but i consider this is another issue.

EDIT:

Basically, i want to do same thing as the caller hack in this answer.

You are right, i done it use caller in a method invoke, though, i still consider i can check where the query method/macro is invoke and manipulate at compile time?

# defined in spec_helper.cr

def query(query_path="")
  if query_path != ""
    graphql_queries_path = "./spec/graphql_queries/#{query_path}.graphql"
  else
    graphql_queries_path = caller[2][/(.+?):\d+.*/, 1].gsub("graphql", "graphql_queries").gsub("_spec.cr", ".graphql")
  end

  if File.exists? graphql_queries_path
    File.read(graphql_queries_path)
  else
    raise "`File #{graphql_queries_path}' not exists!"
  end
end
# invoke in spec/graphql/hello_spec.cr

require "../spec_helper"

p query  # print the content of `spec/graphql_queries/hello.graphql` here

I’d suggest trying to provide the minimum amount of code that reproduces the bug or issue.

IMO I would just make query_path explicit and call it a day.

# Defined in spec_helper.cr
def query(query_path : String)
  File.read "#{__DIR__}/graphql_queries/#{query_path}.graphql"
end

p query "hello"
p query "user/admin"

What would the benefit be? This is all for your tests so it doesn’t really matter. Just do what’s more readable and maintainable.

@Blacksmoke16 , @asterite , i update my original post content for more clear and reproduable.

Yes, use method is better, but for this post, i just curious some behavior of macro, anyway, only tech discuss. :joy:

I just don’t want remember the path, when run query, test will tell me which file is missing, i just add it, another concern is, i am not write ???.grahpql in the same folder as ???_spec.cr, just for make folder more clean.