Search

Dark theme | Light theme

October 1, 2022

Clojure Goodness: Writing Text File Content With spit

In a previous post we learned how to read text file contents with the slurp function. To write text file content we use the spit function. We content is defined in the second argument of the function. The first argument allows several types that will turn into a Writer object used for writing the content to. For example a string argument is used as URI and if that is not valid as a file name of the file to read. A File instance can be used directly as argument as well. But also Writer, BufferedWriter, OutputStream, URI, URL and Socket. As an option we can specify the encoding used to write the file content using the :encoding keyword. The default encoding is UTF-8 if we don't specify the encoding option. With the option :append we can define if content needs to be appended to an existing file or the content should overwrite existing content in the file.

In the following example we use the spit function with several types for the first argument:

(ns mrhaki.sample.spit
  (:import (java.io FileWriter FilterWriter StringWriter File)))

;; With spit we can write string content to a file.
;; spit treats the first argument as file name if it is a string.
(spit "files/data/output.txt" (println-str "Clojure rocks!"))

;; We can add the option :append if we want to add text
;; to a file, instead of overwriting the content of a file.
(spit "files/data/output.txt" (println-str "And makes the JVM functional.") :append true)

;; Another option is the :encoding option which is UTF-8 by default
(spit "files/data/output.txt" (str "Clojure rocks!") :encoding "UTF-8")

;; The first argument can also be a File.
(spit (File. "files/data/file.xt") "Sample")

;; We can pass a writer we create ourselves as well.
(spit (FileWriter. "files/data/output.txt" true) "Clojure rocks")

;; Or use a URL or URI instance.
(spit (new java.net.URL "file:files/data/url.txt") "So many options...")
(spit (java.net.URI/create "file:files/data/url.txt") "And they all work!" :append true)

Written with Clojure 1.11.1.

September 27, 2022

Clojure Goodness: Reading Text File Content With slurp

The slurp funtion in Clojure can be used to read the contents of a file and return it as a string value. We can use several types as argument for the function. For example a string argument is used as URI and if that is not valid as a file name of the file to read. A File instance can be used directly as argument as well. But also Reader, BufferedReader, InputStream, URI, URL, Socket, byte[] and char[]. As an option we can specify the encoding used to read the file content using the :encoding keyword. The default encoding is UTF-8 if we don't specify the encoding option.

In the following example we use the slurp function in different use cases. We use a file named README with the content Clojure rocks!:

(ns mrhaki.sample.slurp
    (:require [clojure.java.io :as io]
              [clojure.test :refer [is]])
    (:import (java.io File)))
  
  ;; slurp interperts a String value as a file name.
  (is (= "Clojure rocks!" (slurp "files/README")))
  ;; Using the encoding option.
  (is (= "Clojure rocks!" (slurp "files/README" :encoding "UTF-8")))

  ;; We can also use an explicit File object.
  (is (= "Clojure rocks!" (slurp (io/file "files/README"))))
  (is (= "Clojure rocks!" (slurp (File. "files/README"))))
  
  ;; We can also use an URL as argument.
  ;; For example to read from the classpath:
  (is (= "Clojure rocks!" (slurp (io/resource "data/README"))))
  
  ;; Or HTTP endpoint
  (is (= "Clojure rocks!" (slurp "https://www.mrhaki.com/clojure.txt")))

Written with Clojure 1.11.1.

DataWeave Delight: Add Conditionally Element to Array or Object

The core of DataWeave is to transform data from one format to another format. If we want to include an element in an array based on a condition we can enclose the element between parenthesis (element) followed by an if statement. If the condition of the if statement returns true the element is included in the array. To include a key/value pair in an object based on a condition we also enclose the key/value pair in parenthesis (key: value) and add an if statement. When the condition specified is true the key/value pair is added to the object, otherwise it will be left out.

In the following example we add conditionally the key website and each element of the array is added based on a condition. The conditions check values in a conf object defined at the top of the example:

Source

%dw 2.0

// Helper object with values used to add elements conditionally.
var conf = {
    kind: "FP",
    includeWebsite: true
}

output application/json
---
[
    ({
        language: "DataWeave", 
        kind: "Functional",
        // Add key/value to object if condition is true.
        (website: "https://docs.mulesoft.com/dataweave/2.4/") if conf.includeWebsite
    }) 
    // Add element to array if condition is true.
    if conf.kind == "FP", 
    
    ({
        language: "Java", 
        kind: "Object Oriented",
        (website: "https://www.java.com/") if (conf.includeWebsite)
    }) if (conf.kind == "OO")
]

Result

[
    {
        "language": "DataWeave",
        "kind": "Functional",
        "website": "https://docs.mulesoft.com/dataweave/2.4/"
    }
]

When we change the values in our conf object we get a different result:

Source

var conf = {
    kind: "OO",
    includeWebsite: false
}

Result

[
    {
        "language": "Java",
        "kind": "Object Oriented"
    }
]

Written with DataWeave 2.4.

DataWeave Delight: Using the update Operator to change values in an object

DataWeave has some very nice features to transform data objects. One of those nice features is the update operator. With the update operator we can change values of keys in an object using a very concise syntax. We don't have to go through all keys and create a new object, but we can pinpoint the exact key and change the value. To get the correct key we use selectors. Once we have the key we can set a new value. We can define a variable to contain the current value if we want to use it to define a new value. Also is it possible to add a condition that needs to be true to change the value. Finally the update operator supports upserting a value if the key might not exist yet.

The complete syntax of the update operator is as follows:

<object> update {
    case <optional variable> at <key selector> <optional condition if (...)> -> <new value> 
}

In the following example we see several use cases of the update operator:

Source

%dw 2.0

var obj = {
    user: {
        firstName: "Hubert",
        lastName: "Klein Ikkink"
    },
    alias: "mrhaki",
    country: "NL"
}

output application/json
---
obj update {    
    // Simply use . selector to get key and set new value
    case .alias ->  "haki"
    
    // Using ! to upsert a key if doesn't exist yet.
    case .likes! -> ["Clojure", "DataWeave", "Groovy"]

    // We can use a variable for the selector value
    // and use it for a new value
    case firstName at .user.firstName -> firstName[0] ++ ".A."

    // DataWeave always provides $ as variable if we don't use at.
    // Here we can use $ to define a new value.
    case .user.lastName -> $ splitBy " " map ((item) -> item[0]) joinBy ""

    // We can add a condition for which we want to update the key
    // with a new value. In this example if country is "NL"
    // the value is "Netherlands", for other values of country
    // no transformation happens.
    case country at .country if (country == "NL") ->  "Netherlands" 
}    

Output

{
    "user": {
        "firstName": "H.A.",
        "lastName": "KI"
    },
    "alias": "haki",
    "country": "Netherlands",
    "likes": [
        "Clojure",
        "DataWeave",
        "Groovy"
    ]
    }    

Written with DataWeave 2.4.

July 13, 2022

Groovy Goodness: Using Macros For Getting More Information About Variables

In a previous post we learned about the macros SV, SVI and SVD that return a string representation of variables with their name and value. Groovy 4 also added the NP and NPL macros that we can use to inspect variables. Instead of returning a GString instance these macros return a NamedValue instance or a list of NamedValue value instances. The NamedValue class is a simple class with a property name, containing the name of the variable, and property val with the value of the variable. The macro NP can be used when we have a single variable and result is a single NamedValue instance. An the macro NVL can be used with multiple variables. The result is a list of NamedValue instances where each instance represent a variable passed as argument to NVL.

In the following example we use NV and NVL:

def languages = ["Java", "Groovy", "Clojure"]

// Use NV macro to create a NamedValue object 
// with name and val properties containing the
// variable name and variable value.
def namedValue = NV(languages)

assert namedValue.name == "languages"
assert namedValue.val == ["Java", "Groovy", "Clojure"]
assert namedValue.class.name == "groovy.lang.NamedValue"


def alias = "mrhaki"
def name = "Hubert"
def age = 49

// We can pass multiple objects with the NVL macro
// and we get a List with multiple NamedValue objects.
def namedValues = NVL(alias, name, age)
assert namedValues.size() == 3
assert namedValues == [new NamedValue("alias", "mrhaki"), new NamedValue("name", "Hubert"), new NamedValue("age", 49)]

// We can use Groovy collection methods.
assert namedValues[0].name == "alias"
assert namedValues[0].val == "mrhaki"
assert namedValues.name == ["alias", "name", "age"]
assert namedValues.val == ["mrhaki", "Hubert", 49]
assert namedValues.find { nv -> nv.name == "age" }.val == 49

Written with Groovy 4.0.3.

Groovy Goodness: Using Macros For Getting String Representation Of Variables

Groovy 4 added some built-in macros that we can use in our code. A macro is code that will create new code. It does this by manipulating the Abstract Syntax Tree (AST) before the code is compiled. So when we use a macro, the macro will change the AST and those changes will be compiled. The three built-in macros SV, SVI and SVD can create a GString instance with the names and values of the variables that are passed as argument. This can be very useful to create some meaningful debugging statements. Normally we would have to type in the variable name ourselves followed by the value. Now with these macros we don't have to type the variable as the macro will add that to the AST for us.

The SV macro will use the toString method of the variable to get the value. When we want to use the inspect method for the value we can use the SVI macro. Finally we can use the SVD macro that uses the dump method to get the variable value.

In the following example we use all three macros with different variables:

def languages = ["Java", "Groovy", "Clojure"]

// With SV the toString method for the object is used.
// The name of the variable is also in our output.
assert SV(languages) == "languages=[Java, Groovy, Clojure]"
assert SV(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"

// With SVI the inspect method for the object is used.
assert SVI(languages) == "languages=['Java', 'Groovy', 'Clojure']"
assert SVI(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"

// We cannot assert here as the output contains the object instance representation
// and that changes with each run.
SVD(languages) // Possible output: languages=<java.util.ArrayList@8f636a77 elementData[Java, Groov, Clojure] size=3 modCount=3>
assert SVI(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"


// We can pass multiple objects to the SV, SVI and SVD macros.
def alias = "mrhaki"
def name = "Hubert"
def age = 49

assert SV(alias, name, age) == "alias=mrhaki, name=Hubert, age=49"
assert SVI(alias, name, age) == "alias='mrhaki', name='Hubert', age=49"
SVD(alias, name, age) // Possible output: alias=<java.lang.String@c077733c value=[109, 114, 104, 97, 107, 105] coder=0 hash=-1065913540 hashIsZero=false>, name=<java.lang.String@817bc072 value=[72, 117, 98, 101, 114, 116] coder=0 hash=-2122596238 hashIsZero=false>, age=<java.lang.Integer@31 value=49>    

Written with Groovy 4.0.3.

July 12, 2022

DataWeave Delight: Define Multi Line Strings

To define a string value in DataWeave we can use both double quotes or single quotes. To define a multi line string we simply define a string value over multiple lines. We don't have to do any strange concatenation, but simply define the value over multiple lines. DataWeave will add an end-of-line character automatically after each line.

In the following example we use different ways to defined single and multi line strings:

Source

%dw 2.0

import lines from dw::core::Strings

output application/json
---
{
    doubleQuotes: "String value defined using double quotes",
    singleQuotes: 'String value defined using single quotes',

    // Multi line strings can be defined as well.
    multiLineDoubleQuotes: "This is a multi line
string value
with double quotes",
    multiLineSingleQuotes: 'This is a multi line
string value with 
single quotes',

    // We can use the lines function to transform
    // each line into an element in an array.
    multiLines: lines("Multiline
string transformed
to array of strings")
}

Output

{
  "doubleQuotes": "String value defined using double quotes",
  "singleQuotes": "String value defined using single quotes",
  "multiLineDoubleQuotes": "This is a multi line\nstring value\nwith double quotes",
  "multiLineSingleQuotes": "This is a multi line\nstring value with \nsingle quotes",
  "multiLines": [
    "Multiline",
    "string transformed",
    "to array of strings"
  ]
}

Written with DataWeave 2.4.

July 11, 2022

DataWeave Delight: Using Literal Types

DataWeave has a nice language feature called literal types. Literal types are types with a single predefined values and can be defined using a String, Number or Boolean value. So the value of a literal type is a fixed value. We can combine multiple literal types into a new type using a union type to define an enumaration in DataWeave. The enumaration can only be one of the literal types used to define it.
Together with overloaded functions literal types are very useful. We can define a function where one of the input arguments is a literal type to define specific behaviour based on the literal type. Then we can overload the function for other literal types with different behaviour. DataWeave will make sure the correct function is called based on the value of the input argument and how it matches to the literal type value.

In the following example we define four new literal types (North, East, South, West), use a union type to define the enumaration Compass and the overloaded function direction:

Source

%dw 2.0

import capitalize from dw::core::Strings

// String based literal types with one value.
type North = "north"
type East = "east"
type South = "south"
type West = "west"

// Using union types we can use several literal
// types to define a new type like an enumeration.
type Compass = North | East | South | West

// Simple function that will be invoked if the input
// argument is either "north" or "south".
fun direction(direction: North | South): String = 
    "You are walking to " ++ 
    // We can still use String functions, 
    // because the base literal type is String.
    capitalize(direction) ++ 
    " pole."

// Overloaded function to "catch" the other values
// for the literal type Compass: "east" and "west".    
fun direction(direction: Compass): String = "Where are you going?"

// Simple data structure with String values.
var data = { up: "NORTH", right: "EAST", down: "SOUTH", left: "WEST" }

output application/json
---
{
    north: direction("north"),
    east: direction("east"),

    // We can coerce a String value into a literal type.
    south: direction(lower(data.down) as Compass),
    west: direction(lower(data.left) as Compass)
}

Output

{
    "north": "You are walking to North pole.",
    "east": "Where are you going?",
    "south": "You are walking to South pole.",
    "west": "Where are you going?"
}

In the following example we use Number and Boolean literal types. Instead of defining them explicitly as types we use the literal type in the function definitions directly:

Source

%dw 2.0

// Overloaded functions with literal types defined 
// directly at the argument level of the function.
// Here we use a Number literal type.
fun displayItems(items: 0) = "You have no items"
fun displayItems(items: 1) = "You have 1 item"
fun displayItems(items: Number) = "You have " ++ items ++ " items"

// Also Boolean literal types are supported.
// We can combine default argument values
// and overloaded functions.
fun message(value: String, debug: true) = "value - " ++ message(value)
fun message(value: String, debug: Boolean = false) = value

output application/json
---
{
    items0: displayItems(0),
    items1: displayItems(1),
    items10: displayItems(10),

    message: message("DataWeave literal types"),
    messageWithDebug: message("DataWeave literal types", true)
}

Output

{
    "items0": "You have no items",
    "items1": "You have 1 item",
    "items10": "You have 10 items",
    "message": "DataWeave literal types",
    "messageWithDebug": "value - DataWeave literal types"
}   

Written with DataWeave 2.4.

July 4, 2022

Groovy Goodness: Closed And Open Ranges

Groovy supports ranges for a long time. But Groovy 4 adds a new feature for ranges and that is the support for open (exclusive) ranges at the beginning of a range. Open means the number that defines the range is not part of the actual range result and we must use the less-than character (<). This is also referred to as exclusive, where the value is excluded from the range. When a range is closed the value is included, also called inclusive. Before Groovy 4 we could already define the end of the range to be exclusive or inclusive, but now we can also define the beginning of the range to be exclusive.

In the following example we use closed and open range definitions from the start or end:

def inclRange = 0..5

assert inclRange == [0, 1, 2, 3, 4, 5]
assert inclRange.from == 0
assert inclRange.to == 5


def exclEndRange = 0..<5

assert exclEndRange == [0, 1, 2, 3, 4]
assert exclEndRange.from == 0
assert exclEndRange.to == 4


// Support for exclusive begin added in Groovy 4.
def exclBeginRange = 0<..5

assert exclBeginRange == [1, 2, 3, 4, 5]
assert exclBeginRange.from == 1
assert exclBeginRange.to == 5


// Support for exclusive begin added in Groovy 4.
def exclRange = 0<..<5

assert exclRange == [1, 2, 3, 4]
assert exclRange.from == 1
assert exclRange.to == 4

Written with Groovy 4.0.3.

Get Absolute Path To An SDK With SDKMAN!

SDKMAN! has a home command that will return the absolute path of any SDK we pass as argument. This can be usefull in scripts where we need the absolute path to a SDK. We can specify the name of the SDK after the home command and the version. To use the current version that is set as default we can use as version current.

In the following example we use the home command for several SDKs:

$ sdk home groovy 4.0.3
/Users/mrhaki/.sdkman/candidates/groovy/4.0.3%
$ sdk home java 17.0.3-tem
/Users/mrhaki/.sdkman/candidates/java/17.0.3-tem%
$ sdk home jbake current
/Users/mrhaki/.sdkman/candidates/jbake/current%
$ export ASCIIDOCTORJ_DIR=`sdk home asciidoctorj current`
$ echo $ASCIIDOCTOR_DIR
/Users/mrhaki/.sdkman/candidates/asciidoctorj/current
$

Written with SDKMAN! 5.15.0.