Skip to content

Latest commit

 

History

History
689 lines (496 loc) · 22.2 KB

README.adoc

File metadata and controls

689 lines (496 loc) · 22.2 KB

Jamal Ruby integration module

Using this integration module, you can mix Jamal macro text with Ruby code snippets. To use this module, you have to add the dependency to your Maven project, as:

<dependency>
    <groupId>com.javax0.jamal</groupId>
    <artifactId>jamal-ruby</artifactId>
    <version>2.6.1-SNAPSHOT</version>
</dependency>
Following that you can use the macros

macros.

Macros implemented in the package

The macros implemented in this package make it possible to embed Ruby scripts into the Jamal source code. Ruby is an object-oriented scripting language that has JVM implementation with compelling features. Using Ruby as a scripting language may be an alternative to using the Java provided and Jamal core integrated JShell. In some cases, an embedded Ruby script may also serve your purpose instead of writing a Java built-in macro.

In this chapter, we will discuss every macro defined in the package with examples. The examples are automatically generated into the Asciidoc file, similarly to the samples in the other documentation files. The difference is that in the case of the main README.adoc and the other files, the conversion uses two Java-defined macros. In this case, we do not use these macros. Instead, Ruby scripts perform the tasks, eliminating the need to write a separate class to implement the user-defined macros sample and output.

The chapter Sample describes this process as a non-trivial example for using the Ruby engine.

The package defines 0 macros.

The macro ruby:eval evaluates a simple expression. ruby:import can import Ruby code. ruby:property can set or get Ruby variables. ruby:shell can execute more complex Ruby code. Finally, ruby:closer makes it possible to implement closer scripts.

The different macros use Ruby ScriptingContainer objects to execute the Ruby scripts. These container objects are automatically created and named. Their use is detailed in the chapter Using Multiple Container Objects. If you do not want to use multiple, separated Ruby containers, you do not need to worry about the container objects. Jamal will create one upon using one of the Ruby macros and utilize it throughout the Jamal file.

i. ruby:eval

The macro ruby:eval uses its whole input as a ruby expression and evaluates it. For example the following macro call: .Jamal source

{@ruby:eval 1+1}

will result:

output
2

ii. ruby:import

This macro can import a Ruby file into the shell. The macro has two forms:

  • {@ruby:import fileName}, and

  • the format

{@ruby:import

}

In the first case, Jamal can import the named file. The file’s name can be relative to the input file containing the import macro, can be absolute, or can have the prefix res: or https://. The file names starting with the prefix res: are resource files and must be available on the classpath. The files with the prefix https:// are downloaded from the net and are cached.

The name resolution and file access, download, and caching are made precisely the same way as in the case of the built-in macro import. For more information about that, please read the main readme file of Jamal.

In the second case, if no characters denote a file name on the first line after the macro name, Jamal will import the macro content.

The shell will evaluate the content. This evaluation tries to create a return value, ignored in the case of the ruby:import macro. However, if the content does not have an evaluable return value, the shell will throw an exception. This exception is hard to ignore because the Jamal macro implementation cannot comprehend if this is a final value evaluation error or an actual Ruby error. If there is an actual Ruby error, then the macros enclose it into a Jamal syntax error and report it that way.

The macro appends a \n'' string after the Ruby script to avoid the final evaluation errors. It will result in an empty string, the macro ruby:import results.

Use ruby:import if you do not want to include the result of the script into your output or if the script is in a separate Ruby file.

iii. ruby:shell

You can execute a script using ruby:shell and get the result as a string into the Jamal output. The format of the macro is

{@ruby:shell scriptname.rb
script content
}

The scriptname.rb part is optional. Only error reports use this name. Getting an error message makes it simpler to find which Ruby script fragment was throwing up. If this part is missing, the macro will use the name of the actual Ruby shell with the .rb extension. Again: this is not a reference to any actual file name; it is only for the error messages. Using something.rb is just a good convention that you may like to follow.

The script content will be evaluated, and the final result will be the result of the macro. Use this macro if your script is longer and not just a simple one-liner, in which case you can use the macro ruby:eval.

iv. ruby:property

This macro sets and gets Ruby variables. Ruby containers can communicate with the embedding application, which is Jamal in our case. The embedding application can set and get variables of the Ruby script.

For example, the following sample

Jamal source
{@ruby:property $myProp=(to_i)55}
{@ruby:eval $myProp+45}

and it will result

output
100

You can also fetch the value of a variable set before by the Ruby script or by the ruby:property macro:

Jamal source
{@ruby:eval $yourProp=133}
{@ruby:property $yourProp}

will result

output
133
133

once by the result of the ruby:eval and once as the ruby:property also fetched this value.

Setting the value, you can specify the type of the property. In the sample above we wrote

Jamal source
{@ruby:property $myProp=(to_i)55}

In that code (to_i) is a conversion and the ruby:property macro will evaluate, and act upon it. The (tp_i) converts the text following it to a Fixnum value. You can use other type conversions following the = between ( and ).

The possible types are limited to

  • to_i to convert the string to a Fixnum

  • to_f to convert the string to Float

  • to_s to convert the string to string. This is the default conversion in case you do not specify any.

  • to_r to convert the string to rational. In this case, the number has to be X/Y formatted.

  • to_c to convert the string to complex number. In this case, the number has to be R+Ii formatted, where R and I are integers or floating-point numbers, and i is the letter i (lower case).

  • to_c/i to convert the string to a complex integer number. It is the same as (to_c), but the real and the imaginary parts of the number have to be integers.

  • to_sym to convert the string to be a Ruby symbol.

The casting type has to be enclosed between ( and ) characters, no spaces are allowed. The default is to set the property to be a string. The casting (to_s) is available if you want to emphasize that the value should be handled as a string. It may also happen that you want to pass a string that starts with the characters (to_i) or something similar.

Some examples:

Jamal source
{@ruby:property complex=(to_c)66+13i}
{@ruby:eval complex * complex}

will result

output
4187.0+1716.0i
Jamal source
{@ruby:property complex=(to_c/i)66+13i}
{@ruby:eval complex * complex}

will result

output
4187+1716i
Jamal source
{@ruby:property ratio=(to_r)66/13}
{@ruby:eval ratio * ratio}

will result

output
4356/169

v. ruby:closer

Using the macro ruby:closer, you can create a so-called closer script. You can use the script to modify the whole output after the processing of Jamal has finished.

The format of the macro is

{@ruby:closer ruby script}

The only argument to the macro is the closer Ruby string. It can be multi-line, and Jamal executes it after processing the whole Jamal file. Before starting the script, the global variable $result will be set. It will contain the result of the Jamal processing. The content of the global variable $result is a Ruby string.

The closer script should result in a string that will replace the original result.

You can specify any number of closer scripts using different or the same Ruby shell. Jamal will invoke all scripts one after the other in the order they were defined in the Jamal source.

Jamal source
Hi, I am the content of the Jamal file.
{@ruby:closer "I do not care what the original text was, replace it with this one."}
The closer will kill me.

will result

output
I do not care what the original text was, replace it with this one.

Using Multiple Container Objects

If you do not specify any shell object, it will be created automatically using the name :ruby_shell.

Jamal stores Ruby shell objects along with the user-defined macros. It has two consequences.

  • If there is a user-defined name with the same name as the Ruby shell name, the one defined later will overwrite the other.

  • The Ruby shell objects are available only within their scopes precisely the same way as user-defined objects. You can also export them.

Note that the default name starts with :; therefore, this is a global name, available in all scopes. It is a feature to ease the use of the Ruby shells when you have only one. It will be created and be available everywhere in the Jamal file, even if the first use of Ruby was in a local scope.

You can overwrite the name of the shell, defining the user-defined macro rubyShell, or using the macro option of the same name or the alias shell.

It can be done using the usual built-in macro define, as in the example

Jamal source
{@ruby:eval $z = 13}
{@define rubyShell=myLocalShell}
{@ruby:eval $z}

will result in the output:

output
13

null

The reason for this is that the first evaluation is executed in a shell named :ruby_shell. The second evaluation, however, runs in a different shell, named myLocalShell.

Note

Note that the try macro use is {@try…​} and NOT {#try…​}. We have to use the ' # ' character to evaluate the content of a built-in macro before the macro invocation. In the case of the try macro, we want to evaluate the content, but NOT BEFORE the try macro invocation. If we use the macro in the form {#try…​}, the content is evaluated before starting the macro try. If there is any error, the macro try cannot catch it because it has not started yet. On the other hand, using {@try…​} will pass the content unevaluated, and the macro try will evaluate it and catch the errors.

It is not Ruby module specific; however, it is a widespread mistake, hence described here.

There is a resource file named ruby.jim. You can import this file and then use the macros defined in it. The previous example will look the following:

Jamal source
{@import res:ruby.jim}
{@ruby:eval $z = 13}
{shell=myLocalShell}
{@ruby:eval $z}

will result in the output:

output
13

null

It is the same as the previous one, not surprisingly.

All Ruby macros are inner scope dependent, which means that you can define the macro rubyShell inside the Ruby macro call. In that case, the definition following the Jamal rules will be local to the Ruby macro.

For example

Jamal source
{@import res:ruby.jim}
{@ruby:eval $z = 13}
{#ruby:eval {shell=myLocalShell}$z}
{@ruby:eval $z = 13}

will result in the output:

output
13
null
13

The second evaluation is performed in a different shell, but the definition of the shell name is local to the macro ruby:eval. (What is more, it is local to the try macro.)

The last example can also be written as

Jamal source
{@ruby:eval $z = 13}
{#ruby:eval (shell=myLocalShell)$z}
{@ruby:eval $z = 13}

will result in the same output:

output
13
null
13

Sample Application, Converting this README.adoc

In this chapter, I will tell the story and the technology used to maintain this documentation file. Several macros are used during the maintenance of the documentation to ensure that the documentation is correct and up-to-date. This particular document’s processing uses Ruby scripts, which are used instead of some built-in macros for demonstration purposes.

The documentation of Jamal is a series of Asciidoc files. The Asciidoc format was invented to be a documentation source format that is easy to read and edit. At the same time, Jamal can also convert it to many different output formats. Asciidoc, however, provides only limited possibility to eliminate redundancy and to ensure consistency. This is where Jamal comes into play.

Jamal’s documentation is maintained in xxx.adoc.jam files, and they are converted to xxx.adoc files. With this workflow, the Asciidoc files are not source files. They are intermediate files along the conversion path. Jamal define macros are used to eliminate text repetition, redundancy whenever it is possible. The Jamal snippet library macros are used to keep the sample codes included in the document up-to-date.

Note

When reading this part of the documentation, you are probably familiar with the basic functionalities of Jamal. If you need to refresh the memory, then read the documentation in the project’s root folder. Snippet macros are documented in the Snippet README.adoc file. It is unnecessary to know and understand how the snippet macros work to read this chapter, but it is a recommended read in general.

Technical documentation using Jamal and the snippet macros usually generates the documentation in multiple steps.

  • Run the tests, including the sample code, and capture the sample output in one or more output files.

  • Process the Jamal source of the documentation and include from the source code and the generated sample output files the samples.

For example, a Java application can support the documentation with unit test samples. Some of the unit tests serve the purpose of testing only, while others are there to document specific code parts. The output of the documentation purposed tests is captured into output files. The test file jamal-ruby/src/test/java/javax0/jamal/ruby/TestRubyMacros.java contains

// snippet sample_snippet
@Test
@DisplayName("Test that ruby conversion to fixnum works")
void testRubyPropertyFixNum() throws Exception {
    TestThat.theInput(
        "{%@define rubyShell=wuff%}" +
            "{%@ruby:property int=(to_i)5%}" +
            "{%@ruby:shell\n" +
            "  (int*int)\n" +
            "%}"
    ).usingTheSeparators("{%", "%}").results("25");
}

// end snippet

To get this content into the document what we have to write is the following:

[source,java]
----
// snippet sample_snippet
{%@snip sample_snippet %}\
// end snippet
----

The output generated (none in this case) can also be included using the snip macro.

It is logical to run the tests and generate the test output in an initial step in the case of Java. However, when we test and document Jamal processing, it is a logical idea to use the Jamal environment, which is converting the documentation. The external approach with an initial step is also possible, but it is not needed.

The sample Jamal code can be included in the documentation as a code sample. Using Jamal macros, Jamal can also convert it to the corresponding output, which can also be included in the resulting document without saving it into an intermediate file.

To do that, the Jamal Snippet package unit test file jamal-snippet/src/test/java/javax0/jamal/documentation/TestConvertReadme.java uses a built-in macro, implemented in the file:

  • jamal-snippet/src/test/java/javax0/jamal/documentation/Output.java

This Java implemented macro is available on the classpath when the unit test runs.

Note

Executing the Jamal processing of a Java software package documentation via the unit tests has other advantages. The macros java:class and java:method can check that the class and method names referenced in the document are valid. Class and method names may change during refactoring. The documentation many times does not follow this change and becomes stale. When the classes and methods are referenced using these macros, they throw an exception if the class or method does not exist.

This class is very simple:

public class Output implements Macro {
    final Processor localProc = new javax0.jamal.engine.Processor("{", "}");

    @Override
    public String evaluate(Input in, Processor processor) throws BadSyntax {
        return localProc.process(new javax0.jamal.tools.Input(in.toString(), in.getPosition()));
    }
}

It creates a single Jamal processor instance and uses it to evaluate the input passed to it. This macro runs a Jamal processor separate from the Jamal processor that is converting the document. However, the two Jamal processors run in the same JVM, and one is invoking the other through this built-in macro.

To simplify the use, there is a readmemacros.jim macro import file that defines the user-defined macro sample and output. (A built-in macro can have the same name as a user-defined.) The macro sample results in its content in Asciidoc code sample format, adding [source]\n---- before and ---- after the sample code. At the same time, it also saves the sample code in a user-defined variable called lastCode. The macro output uses the lastCode and using the built-in output from the Output.java displays the calculated result as a code block.

It is very similar when we are using Ruby, but in this case, we do not need the built-in macro output. When Jamal converts this document, the readmemacros.jim` inside the jamal-ruby directory contains some Ruby scripts instead of the built-in macros.

The unit test code that invokes the Jamal processor to convert this document is the following:

final var processor = new Processor("{%", "%}");
final var in = FileTools.getInput(directory + "/" + fileName + "." + ext + ".jam", processor);
final var shell = Shell.getShell(processor, Shell.DEFAULT_RUBY_SHELL_NAME);
shell.property("$processor", new MyProcessor());
processor.defineGlobal(shell);
final var result = processor.process(in);

It is almost a standard invocation of the Jamal processor. The only difference is that it creates a Ruby container using the default container name and injects a Jamal MyProcessor instance into the container with the name $processor. When Jamal runs any Ruby code running in the same container will be able to access the processor.

The MyProcessor class is an inner class inside the test class, and it reads as the following:

public static class MyProcessor {
    final Processor processor = new Processor("{", "}");

    public String process(String s) throws BadSyntax {
        return processor.process(Input.makeInput(s));
    }
}

This class is a wrapper, that provides the method process() with a string argument. It can directly be invoked from Ruby.

Using this possibility the user defined macros sample and output are simply the following:

  • sample

    {%@define sample(code)=[source]
    ----
    {%#trimLines
    {%@ruby:property $lastCode=(to_s)code%}{%@ruby:shell
    while $lastCode.length > 0 and $lastCode[0] == '\n'
        $lastCode = $lastCode[1..-1]
    end
    while $lastCode.length > 0 and $lastCode[$lastCode.length-1] == '\n'
        $lastCode = $lastCode[0..-2]
    end
    $lastCode
    %}%}
    ----%}

This macro saves the sample code to the global Ruby variable $lastCode calling the macro `ruby:property. The script removes the leading and trailing newline character from the sample, if there is any. Finally, the script returns the resulted string, which is placed between the Asciidoc code display.

  • output

    {%@define output=[source]
    ----
    {%#trimLines
    {%#ruby:shell
    $processor.process($lastCode)%}%}
    ----
    %}

This macro uses the saved property lastCode to access the text of the last sample. It invokes the processor to process it. The result value of the macro is the output of the processor.

This chapter discussed how documentation should be "programs" to avoid redundancy in the source and to support consistency. After that, we made a short detour discussing the Jamal snippets, which have complete documentation in the file Snippet README. We also discussed how the documentation conversion works with snippets and Jamal samples in the Snippet module. Finally, we had a look at how simpler it is using the Ruby integration.

Note
We copied none of the sample codes manually in the source README.adoc.jam.

It demonstrates the power and flexibility of Jamal enhanced with the Ruby integration. If you like the idea, but Ruby is not your favorite scripting language, have a look at the Groovy Integration documentation and give it a try.

Loading the macros

Starting with the version 2.0.0 the library is not configured to be on the class path of the command line version or the Asciidoctor preprocessor. The reason is security. The interpreter, just as well as the Groovy and ScriptBasic interpreters, can execute arbitrary code. If you want to use the ScriptBasic interpreter you have to

  • modify the property maven.load.include and maven.load.exclude in the file ~/.jamal/settings.properties to include the ruby module. For example:

    maven.load.include=com.javax0.jamal:jamal-ruby:2.6.1-SNAPSHOT
  • add the line

    {@maven:load com.javax0.jamal:jamal-ruby:2.6.1-SNAPSHOT}

    to the Jamal file where you want to use the ScriptBasic interpreter.

  • To include the resource file ruby.jim you have to add the line

    {@import maven:com.javax0.jamal:jamal-ruby:2.6.1-SNAPSHOT::ruby.jim}

    instead importing it as a resource.