JavaScriptのthisについて

関数宣言とArrow Functionのthisの違いについてのメモです。

以下のサイトに詳しく書いてあります。

関数宣言の場合、thisは動的に決まるとのこと。

ベースのオブジェクトがあればそのオブジェクトをthisとし、なければundefinedとなる、と。

ここで言うベースのオブジェクトとはブラケット演算子、ドット演算子の左側にあるオブジェクトのことです。

では、実験してみましょう。

環境はこちら。

❯ node --version
v20.11.1

REPLを使っていきます。ちょっと試したいときはREPLが使いやすいと個人的には思います。

次のようにthisをコンソールに出力してみます。

関数宣言の場合、ただ呼び出すとundefinedになります。これはベースとなるオブジェクトがないからですね。

> function hoge() {
... console.log(this);
... }
> hoge();
undefined

このhoge関数をhogeObjオブジェクトにセットしてみます。

そうすると、thisはhogeObjになりました。thisが実行時のコンテキストで動的に変わっているのがわかりました。

> const hogeObj = {
... hoge
... }
> hogeObj.hoge();
{ hoge: [Function: hoge] }

callを使えばベースとなるオブジェクトを指定しつつ呼び出すことができます。

> hoge.call({});
{}

同じようにArrow Functionでも試してみましょう。

> const fuga = () => {
... console.log(this);
... }
> fuga();
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

Arrow Functionの場合はthisを外側のスコープに探しに行きます。

node.jsの場合はglobalオブジェクトがグローバルスコープのthisのようです。

https://nodejs.org/dist/latest-v20.x/docs/api/globals.html#global-objects

そういえばdenoはどうなんだろう?

❯ deno --version
deno 1.41.0 (release, x86_64-unknown-linux-gnu)
v8 12.1.285.27
typescript 5.3.3
> this
Window {}

Windowオブジェクトが取れました。実行環境で違いますね。

話を戻してArrow Functionのベースオブジェクトを変更してみます。

> const fugaObj = {
... fuga
... }
> fugaObj.fuga();
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

> fuga.call({});
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

オブジェクトにセットしても、callをつかっても、結果が変わらないことがわかります。

というわけで、Arrow Functionの場合はthisが静的に決定されあとから変更することができないことがわかりました。

一方で、Arrow Function自体を動的に生成する場合、外側のthisが動的に変わるのであればもちろんArrow Functionが参照するthisも生成されるごとにことなるthisを参照することになります。

例えば次のようなケースです。

> const hoge = {
... fuga() {
...   return () => { console.log(this) };
... }
... }
> hoge.fuga()();
{ fuga: [Function: fuga] }
> hoge.fuga.call({})();
{}

hogeオブジェクトのfugaメソッドはArrow Functionを作成して返します。

Arrow Functionはthisを外側のスコープに探しに行きます。このときの外側のスコープにはfugaメソッドであり、fugaメソッドのthisはhogeオブジェクトです。

なので単純に hoge.fuga()() と呼び出すとArrow Functionのthisはhogeオブジェクトになりました。

ただし、fugaメソッドの呼び出しをcallで行いベースオブジェクトを変更するとArrow Functionのthisも変化します。

これはArrow Functionが作成されるとき、外側のスコープを探しに行くためです。

fugaメソッドのthisが変更されたのであれば変更されたthisをArrow Functionも探しに行くことになる、ということですね。

jacksonその2:TreeModel

jacksonについて調べたときのメモその2です。

環境は前回の記事と同じですので省略します。

https://baubaubau.hatenablog.com/entry/2024/02/04/145625

TreeModelというものがどうやらあるらしいので触ってみましょう。

https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#tree-model

一応wikiにもページがありましたが中身が空っぽのようです。

https://github.com/FasterXML/jackson-databind/wiki/JacksonTreeModel

TreeModel

jacksonを使うとJavaのオブジェクトとJSONまたは、ListやMapなどのコレクションなどとJSONとの間で相互変換ができますが、JSONをトラバーサルするのには向いていません。ということでTreeModelというものがあるぞ、というみたいです。

ObjectMapperをインスタンス化し、

ObjectMapper objectMapper = new ObjectMapper();

readTreeでJSON文字列を読み込みます。するとJSONのルート要素を表現するJsonNodeが返ってくるようです。

JsonNode root;

root = objectMapper.readTree("""
    {
        "NB001": {
            "profile": {
                "firstName": "book",
                "lastName": "store",
                "age": 25
            },
            "type": [
                "software engineer"
            ]
        }
    }
    """);

トラバーサルして値を読み込むにはgetを使い目的のプロパティまで掘っていけば良さそう。

値の型に応じてasなんちゃらメソッドを呼び出せば対応するJavaの値で取得できます。

String firstName = root.get("NB001").get("profile").get("firstName").asText();
assertEquals("book", firstName);

int age = root.get("NB001").get("profile").get("age").asInt();
assertEquals(25, age);

値がない場合、結果はnullになりました。

JsonNode notExist = root.get("notExist");
assertNull(notExist);

値と型が適切ではない場合も試してみました。

どうやら変換できるものは頑張ってくれますが、無理なものは無意味な値で取得されるようです。てっきり例外が発生するかと思いましたがそうではないみたい。

int firstName = root.get("NB001").get("profile").get("firstName").asInt();
assertEquals(0, firstName); // 本当は "book"

String age = root.get("NB001").get("profile").get("age").asText();
assertEquals("25", age); // 数値の25が文字列に変換された

値を取得する前に対象がどんな型なのかを確認するには、isなんちゃらのメソッドを使えば良さそうです。

JsonNode firstNameNode = root.get("NB001").get("profile").get("firstName");
assertTrue(firstNameNode.isTextual());
assertFalse(firstNameNode.isInt());

JsonNode ageNode = root.get("NB001").get("profile").get("age");
assertTrue(ageNode.isInt());
assertFalse(ageNode.isTextual());

トラバーサるしつつ、JSONの一部分をオブジェクトへマッピングできるようです。

次のようなクラスを定義し、

public record Profile(String firstName, String lastName, int age) {
}

treeToValueで読み込みます。

Profile profile = objectMapper.treeToValue(root.get("NB001").get("profile"), Profile.class);
assertEquals(new Profile("book", "store", 25), profile);

そういえば、jacksonはネストされた構造のJSONを読み込むとどうなるのでしょうか?

こういうクラスを用意し、

record ProfileNestedJson(String firstName, String lastName, int age, ProfileNestedJsonAddress address) {
}
                                                                                                        
record ProfileNestedJsonAddress(String country, String countryCode) {
}

以下のように読み込めました。

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);

var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class);
                                                                                                                          
assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);

Javaのクラスにはないプロパティを持つJSONはどうなるのでしょうか?

次のようにpostalCodeを追加してJSONを読み込むと例外が発生しました。てっきり無視されるかと思いましたが。

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN",
                        "postalCode": "000-000"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);
var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class);
                                                                                                                          
assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);

例外は次の通り。丁寧にignorableじゃないぞと書かれています。

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "postalCode" (class org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress), not marked as ignorable (2 known properties: "countryCode", "country"])
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: org.example.TreeModelTest$TreeToValueTest$ProfileNestedJson["address"]->org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress["postalCode"])

    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1153)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2224)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1719)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1697)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:279)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:464)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:571)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:440)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
    at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4875)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3033)
    at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3497)
    at org.example.TreeModelTest$TreeToValueTest.treeToValueNestedJsonAdditionalProperties(TreeModelTest.java:157)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

どうやら @JsonIgnoreProperties で無視するプロパティを指定する必要があるとのこと。

https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#annotations-ignoring-properties

というわけで次のようにしたら動きました。

record ProfileNestedJsonAdditionalProperties(String firstName,
                                             String lastName,
                                             int age,
                                             ProfileNestedJsonAdditionalPropertiesAddress address) {
}
                                                                                                    
@JsonIgnoreProperties("postalCode")
record ProfileNestedJsonAdditionalPropertiesAddress(String country, String countryCode) {
}

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN",
                        "postalCode": "000-000"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);
var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJsonAdditionalProperties.class);
                                                                                                                                
assertEquals(new ProfileNestedJsonAdditionalProperties("book", "store", 25,
        new ProfileNestedJsonAdditionalPropertiesAddress("Japan", "JPN")), profileNestedJson);

jacksonその1

jacksonについて調べてみたときのメモです。

jacksonのリポジトリはこちら。

https://github.com/FasterXML/jackson

jacksonとは、JavaJSONライブラリ、というところまでは前提知識があります。

以前からそれとなく使っていて、Spring frameworkを使っていればフレームワークがデフォルトで使用しているはずです。

jacksonは3つの要素から構成されているようです。

他にもコミュニティによるモジュールがあるようですが、上記のモジュールはjackson開発チームで開発、メンテナンスがされているそう。

サードパーティのモジュール一覧はこちらに記載がありました。思っていたより多いです。

https://github.com/FasterXML/jackson?tab=readme-ov-file#third-party-datatype-modules

databindリポジトリチュートリアルがありました。やってみましょう。

https://github.com/FasterXML/jackson-databind

環境

java -version
openjdk version "17.0.8.1" 2023-08-24
OpenJDK Runtime Environment Temurin-17.0.8.1+1 (build 17.0.8.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.8.1+1 (build 17.0.8.1+1, mixed mode, sharing)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jackson-my-playground</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.16.0</jackson.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.10.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

read and write

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ReadWriteTest {

    @Test
    void readValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var person = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Person.class);
        assertEquals("firstName: book, lastName: store", person.toString());
    }

    @Test
    void writeValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var output = objectMapper.writeValueAsString(new Person("book", "store"));
        assertEquals("{\"firstName\":\"book\",\"lastName\":\"store\"}", output);
    }

    public static class Person {

        @SuppressWarnings("unused") // jackson use
        public Person() {

        }

        public Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String firstName;

        public String lastName;

        @Override
        public String toString() {
            return String.format("firstName: %s, lastName: %s", firstName, lastName);
        }

    }

}

jacksonによるJSONの読み込み、書き込みはObjectMapperを使います。ObjectMapperクラスはインスタンス化することで使用できます。

JSONPOJOで読み込むにはreadValueを使います。反対に書き込むにはwriteValueを使います。

書き込んだ結果をStringで貰いたいのであれば、writeValueAsStringを使います。

読み込む先のPOJOはデフォルトコンストラクタ(引数なしコンストラクタ)が必要になりました。おそらくですが、読み込み元のJSONPOJOにどうやって変換するかどうかは変更できると思うので、上記のコードのようにデフォルトでは必要ということでしょう。

collection

JSONの読み込み、書き込みはPOJOだけではありません。以下のようにCollectionのMapやListを指定することも可能です。

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class GenericCollectionTest {

    final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void readAsMap() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        Map<String, String> result = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Map.class);
        assertEquals(Map.of("firstName", "book", "lastName", "store"), result);
    }

    @Test
    void readAsList() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        List<Integer> result = objectMapper.readValue("""
                [1, 2, 3]
                """, List.class);
        assertEquals(List.of(1, 2, 3), result);
    }

    @Test
    void cantReadObjectToList() {
        assertThrows(MismatchedInputException.class, () -> objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, List.class));
    }

    @Test
    void readAsMapPersonRecord() throws JsonProcessingException {
        var people = objectMapper.readValue("""
                {
                 "NB001": {
                  "firstName": "book",
                  "lastName": "store"
                 }
                }
                """, new TypeReference<Map<Id, Person>>() {
        });
        assertEquals(Map.of(new Id("NB001"), new Person("book", "store")), people);
    }

    public record Id(String value) {
    }

    public record Person(String firstName, String lastName) {
    }

}

readAsMapPersonRecordではMapで読み込みをしつつ、valueにRecordクラスを指定しました。

このように、JSONをどのように変換するのか一つの型に収まらない場合はTypeReferenceを使えば良さそうです。

短絡評価:JavaScriptにおける値の評価順序と動作

  • 値が定まった時点で評価をそれ以上行わないことを短絡評価と呼ぶ。
  • 例えば[JavaScript]では && 演算子が該当する。
  • && は左から順に評価しtrueだった場合は右辺の評価を続ける。
    • 右辺が最後の評価対象の場合はその評価結果を返す。
    • true以外だった場合(false, undefined, 2)評価をそれ以上行わず、その時点の評価結果を返す。
  > true && console.log("hello");
  hello
  undefined
  > false && console.log("hello");
  false
  > console.log("hello") && "msg";
  hello
  undefined
  • && は暗黙的な型変換を行い、値を真偽値に変換する。
  • false に変換される値はfalsyな値と呼ばれる。

複数つなげた場合の動作

  • && を複数つなげて書くと短絡評価は左から右へと順番に処理される。
  • すべての値がtrueの場合の動作。
    • 1番目の値を評価し、trueなので真ん中の値を評価する。
    • 2番目の値を評価し、trueなので最後の値を評価する。
    • 3番目の値を評価し、結果として返す。
   > true && true && 'hello 1'
   'hello 1'
   > 'hello 1' && 'hello 2' && 'hello 3'
   'hello 3'
  • 途中の値がfalseの場合の動作。
    • 中央の値を評価し、falsyな値のためその値を結果として返す。
   > true && null && 'hello'
   null
  • 最後の値がfalseの場合の動作。
    • 最後の値を評価し、falsyな値のためその値を結果として返す。
   > true && true && null
   null

Gaugeでscenarioごとにオブジェクトを初期化する

Gaugeのドキュメントを眺めていたら気がついたのでメモします。

Gaugeのドキュメントっていうのはこちら↓です。

docs.gauge.org

GaugeをJavaで利用しているのであれば、 env/default/java.properties の設定でscenarioまたは特定のスコープでオブジェクトを初期化することができます。というか、デフォルトでscenarioごとに初期化されます。初期状態で以下のように設定されているはずです。

# specify the level at which the objects should be cleared
# Possible values are suite, spec and scenario. Default value is scenario.
gauge_clear_state_level = scenario

この .properties ファイルは言語ごとの設定を書くファイルのようです。

例えば次のようなstepの実装があったとします。この httpResponse はscenarioごとに初期化されnullに戻されます。

package playground.booookstore

import com.thoughtworks.gauge.Step
import io.github.nomisrev.JsonPath
import io.github.nomisrev.path
import io.github.nomisrev.string
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlin.test.assertEquals

class Steps {

    private val httpClient = HttpClient(CIO)

    private var httpResponse: HttpResponse? = null

    @Step("オーダーIDが<orderId>であるオーダーを取得する")
    fun getOrderByOrderID(orderId: String) = runBlocking {
        httpResponse = httpClient.get("http://localhost:8080/order/$orderId")
    }

    @Step("レスポンスステータスが<httpResponseStatusCode>である")
    fun assertHttpResponseStatusCode(httpResponseStatusCode: String) {
        assertEquals(200, httpResponse!!.status.value)
    }

    @Step("レスポンスのJSONの<jsonPath>は<orderId>である")
    fun assertRespondOrderId(jsonPath: String, orderId: String) = runBlocking {
        val body = httpResponse!!.body<String>()
        val respondJsonElement: JsonElement = Json.decodeFromString<JsonElement>(body)
        val jsonPathOrderId = JsonPath.path(jsonPath).string
        assertEquals(orderId, jsonPathOrderId.getOrNull(respondJsonElement), "respond body is $body")
    }

}

scenarioごとで使いまわしたいのであれば、次のようにspecification headingの下に対象のオブジェクトを初期化するstepを持ってくるようにします。

# GET /order/{id}
* オーダーIDが"ad86ffdd-d891-4261-b29b-76ee631c28fa"であるオーダーを取得する

## オーダーIDを指定しオーダーを取得できる
* レスポンスステータスが"200"である
* レスポンスのJSONの"id"は"ad86ffdd-d891-4261-b29b-76ee631c28fa"である
* レスポンスのJSONの"orderDateTime"は"2023-09-23T22:18:11"である

## ショップ情報を取得できる
* レスポンスのJSONの"shop.id"は"SA0078"である
* レスポンスのJSONの"shop.name"は"さいたま川越一号店"である

kotlin coroutine async CoroutineStart.LAZY の使い方メモ

例えば async を使った次のコードがあるとします。

class ComposingSuspendingFunctions {

    private suspend fun doSomethingUsefulOne(): Int {
        delay(100L)
        return 13
    }

    private suspend fun doSomethingUsefulTwo(): Int {
        delay(1000L)
        return 29
    }

    @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
    @Test
    fun concurrentUsingAsync() = runTest {
        val time = testTimeSource.measureTime {
            val one = async { doSomethingUsefulOne() }
            val two = async { doSomethingUsefulTwo() }
            println("${one.await() + two.await()}")
        }
        assertEquals(Duration.parse("1s"), time)
    }
}

doSomethingUsefulOnedoSomethingUsefulTwo をそれぞれ非同期的に呼び出しているので1秒で処理が完了していますね。

asyncは(もしかするとJobも?)必要になったタイミングでコルーチンを実行することもできるようです。

    @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
    @Test
    fun lazilyStartedAsync() = runTest {
        val time = testTimeSource.measureTime {
            val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
            val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
            println("${one.await() + two.await()}")
        }
        assertEquals(Duration.parse("1.1s"), time)
    }

これは、 one.await() のタイミングで実際に計算が走ります。次に two.await() でも計算が走ります。任意のタイミングで、コルーチンを実行できるということです。

これを使えば不要なコルーチンを実行せずにすむため、リソースの節約になりそうです。

配列のパラメータ展開

BashScriptでは、配列をパラメータ展開する方法が用意されています。ちなみに、よく混合しやすいのがパラメータ置換ですが、パラメータ置換とパラメータ展開は本質的に異なるものです。パラメータ置換はパラメータの値を置き換えることでパラメータ展開はパラメータの値を取り出すことです。

@* はどちらも配列のすべての要素を取得する方法ですが重要な違いがあります。これは言葉で説明するよりも実際のコードを見たほうがわかりやすいでしょう。

#!/bin/bash
set -- a b c

for i in "$@" ; do
  echo "$i"
done
: <<EOF
a
b
c
EOF

for i in "$*" ; do
  echo "$i"
done
echo
: <<EOF
a b c
EOF

1つ目のforループでは各要素ごとに値を取り出しループ処理しています。一方で2つ目のforループではすべての要素を一つに結合しており実際にループは一回で終わります。

つまり、 * を使うとすべての要素が結合されます。 @ を使うと個々の要素が分解されて展開されます。

* を使った配列の展開ではIFSで要素を結合するときの文字列を指定できます。あまりないかもしれませんが、文字列を結合して出力するときのフォーマットとして使えるかもしれません。

#!/bin/bash
set -- a b c

OLD_IFS=IFS
IFS=','

for i in "$*" ; do
  echo "$i"
done
echo
: <<EOF
a,b,c
EOF

IFS=OLD_IFS

Written with StackEdit.