Blue Prism で Object の仕様書を自動生成する

# はじめに

Blue Prism では、Object というアプリケーションを実際に操作する部品と、Process というビジネスロジックをかく部品のレイヤーが分かれていて、再利用性を高める仕組みになっています。
チーム内で Object の再利用を進めるには、「どういう Object があるのか」というドキュメントを作ってメンテナンスする必要がありますが、手で作ったりメンテナンスするのは辛かったり、Objectの内容がアップデートされているのに追従できなかったり、という問題が生じる可能性があります。

この記事では、そうした重要な Object に関するドキュメント（Blue Prism 用語でいうところの ODI : Object Design Instruction）を自動で生成しよう、という話を扱います。

#  「Object Inventory」 というVBOがある！ が動かない。。。

[Ditigal Exchange に 「Object Inventory」](https://digitalexchange.blueprism.com/dx/entry/3439/solution/object-inventory) という VBO があるのですが、文字列のトリムが英語前提になっていたり、謎の Excel エラー（```Exception : HRESULT からの例外:0x800A03EC```）が出たりして、うまく動きませんでした。。。

ただ、そのヘルプページに下記の記述がありました。

 ```This asset is a business object that uses the output from the BP command line /getbod function to create a list of all business objects, pages, descriptions, inputs and outputs. ```

AutomateC.exe に ```/getbod``` というスイッチがあるということは、ヘルプの[コマンドラインオプション](https://bpdocs.blueprism.com/bp-6-8/ja-jp/helpCommandLine.htm)にも書かれていないです。。。

# /listprocesses と /getbod というスイッチ
先の「Object Inventory」VBOを覗いてみると、 ```/listprocesses``` スイッチで Process と Object の一覧を取得し、それぞれについて ```/getbod``` を呼んでいるようです。対象が Process だと、``` Could not find Business Object``` という文字列がかえってくるので、それらは処理対象から外す、という流れのようです。

```/getbod``` スイッチは実在した！

# python で実装してみる

拙いスクリプトですが、動作します。ご参考になれば幸いです。（Python 3.7.4で検証しました）

## listprocesses したものについて、順次 getbod したものをテキストファイルに保存する

```python
"""
Blue Prism に接続するためのパラメーターを BP_USERNAME, BP_PASSWORD, BP_DBCONNAME 環境変数に設定して実行してください。

BP_USERNAME : ユーザー名
BP_PASSWORD : パスワード
BP_DBCONNAME : 接続名
"""
import delegator
from pathlib import Path
import logging
import os

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

bp_username = os.environ["BP_USERNAME"]
bp_password = os.environ["BP_PASSWORD"]
bp_dbconname = os.environ["BP_DBCONNAME"]

LISTPROCESSES_CMD = '"C:\Program Files\Blue Prism Limited\Blue Prism Automate\AutomateC.exe" /user {bp_username} {bp_password} /dbconname {bp_dbconname} /listprocesses'
command = LISTPROCESSES_CMD.format(
    bp_username=bp_username, bp_password=bp_password, bp_dbconname=bp_dbconname
)
context = delegator.run(command)
object_list = context.out
object_names = object_list.splitlines()
logger.info(object_names)


GETBOD_CMD = '"C:\Program Files\Blue Prism Limited\Blue Prism Automate\AutomateC.exe" /user {bp_username} {bp_password} /dbconname {bp_dbconname} /getbod "{object_name}"'

for object_name in object_names:
    command = GETBOD_CMD.format(
        bp_username=bp_username,
        bp_password=bp_password,
        bp_dbconname=bp_dbconname,
        object_name=object_name,
    )
    context = delegator.run(command)
    description = context.out
    if (
        len(description.splitlines()) <= 1
    ):  # Process は説明が "XXというビジネスオブジェクトが見つかりませんでした" という1行だけ出力される
        logger.info("{} is not a object".format(object_name))
        continue
    # ファイル名に slash がはいっているとファイルを作成できなくなるので置換
    description_file_name = object_name.replace("/", "_") + ".txt"
    with open(Path("output_descriptions") / description_file_name, "w") as f:
        f.write(context.out)

```

## getbod したテキストファイルを markdown にして保存する

もっと簡単にパースできるライブラリなどあったらどなたか教えてください。。。

```python
from typing import List, Optional
from dataclasses import dataclass, field
import re
import enum
import logging
from pathlib import Path

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


@dataclass
class VBOActionDescription:
    """
    VBO の各アクションの情報を保持するクラス
    """

    action_name: str
    description: str = ""
    pre_condition: str = ""
    post_condition: str = ""
    input_params: List[str] = field(default_factory=list)
    output_params: List[str] = field(default_factory=list)

    def as_markdown(self) -> str:
        """
        Markdown 形式で表現する
        """
        _md = (
            "###{action_name}\n"
            "{description}\n"
            "\n"
            "####事前条件\n"
            "{pre_condition}\n"
            "\n"
            "####事後条件\n"
            "{post_condition}\n"
            "\n"
            "####入力パラメーター\n"
            "{input_params}\n"
            "\n"
            "####出力パラメーター\n"
            "{output_params}\n"
            "\n"
        )
        input_params_md = ("\n").join(
            ["* {}".format(input_param) for input_param in self.input_params]
        )
        output_params_md = ("\n").join(
            ["* {}".format(output_param) for output_param in self.output_params]
        )

        out = _md.format(
            action_name=self.action_name,
            description=self.description,
            pre_condition=self.pre_condition,
            post_condition=self.post_condition,
            input_params=input_params_md,
            output_params=output_params_md,
        )
        return out


class VBODescription:
    """
    VBO の情報を保持するクラス。VBOActionDescription のリストを小として持つ
    """

    def __init__(
        self,
        object_name: str,
        description: Optional[str] = "",
        mode: str = "",
        actions: Optional[List[VBOActionDescription]] = None,
    ):
        self.object_name = object_name
        self.description = description
        self.mode = mode
        self.actions = actions

    def as_markdown(self) -> str:
        """
        Markdown 形式で表現する
        """
        _md = (
            "#{object_name}\n"
            "{description}\n"
            "\n"
            "##動作モード\n"
            "{mode}\n"
            "\n"
            "##アクション\n"
        )

        out = _md.format(
            object_name=self.object_name, description=self.description, mode=self.mode
        )
        if self.actions:
            out = out + ("\n").join([action.as_markdown() for action in self.actions])

        return out


class DescriptionOfWhat(enum.Enum):
    """
    VBO の説明文をパースする際に、「どの部分を読んでいるか」という情報が必要になったため設けた Enum
    """

    business_object = "Business Object"
    action = "Action"
    pre_condition = "Pre Conditiion"
    post_condition = "Post Conditiion"


def classify_line(line: str):
    """
    行の分類を行う
    """
    line = line.strip()
    # =ビジネスオブジェクト - Utility - File Management=  <=左のような行にマッチ
    match = re.search("^=(?P<content>[^=]*)=$", line)
    if match:
        return {"type": "object name", "content": match.groupdict()["content"]}
    # このビジネスオブジェクトの実行モードは「background」です  <=左のような行にマッチ
    match = re.search("^このビジネスオブジェクトの実行モードは「(?P<mode>[^」]+)」です", line)
    if match:
        return {"type": "mode", "content": match.groupdict()["mode"]}
    # ==Append to Text File==  <=左のような行にマッチ
    match = re.search("^==(?P<content>[^=]*)==$", line)
    if match:
        return {"type": "action name", "content": match.groupdict()["content"]}
    # ===前提条件===  <=左のような行にマッチ
    match = re.search("^===(?P<content>[^=]*)===$", line)
    if match:
        content = match.groupdict()["content"]
        if content == "前提条件":  # Blue Prism側の翻訳が変。。。
            return {"type": "pre condition", "content": content}
        if content == "エンドポイント":  # Blue Prism側の翻訳が変。。。
            return {"type": "post condition", "content": content}
        # それ以外
        return {"type": "action attribute", "content": content}
    # *入力:File Path (テキスト) - Full path to the file to get the file size   <=左のような行にマッチ
    match = re.search("^\*入力:(?P<content>.*)$", line)
    if match:
        return {"type": "input parameter", "content": match.groupdict()["content"]}
    # *出力:File Path (テキスト) - Full path to the file to get the file size  <=左のような行にマッチ
    match = re.search("^\*出力:(?P<content>.*)$", line)
    if match:
        return {"type": "output parameter", "content": match.groupdict()["content"]}
    # それ以外の行
    return {"type": "article", "content": line}


def append_action_to_vbo_description(latest_action, vbo_description):
    actions = vbo_description.actions
    if actions:
        vbo_description.actions.append(latest_action)
    else:
        vbo_description.actions = [
            latest_action,
        ]
    return vbo_description


def convert_to_markdown(bod_description_filepath) -> str:
    """
    Markdown に変換する処理の本体
    """
    vbo_description = None
    with open(bod_description_filepath, "r", encoding="shift_jis", newline="\r\n") as f:
        previous_line_type: Optional[DescriptionOfWhat] = None  # いまどの内容を読んでいるか、を保持する。
        latest_action = None

        for line in f:
            line_class = classify_line(line)
            if line_class["type"] == "object name":
                vbo_description = VBODescription(line_class["content"])
                previous_line_type = DescriptionOfWhat.business_object
                continue
            if line_class["type"] == "mode":
                assert vbo_description, "実行モードが正しい位置で記述されていません"
                vbo_description.mode = line_class["content"]
                continue
            if line_class["type"] == "article":
                assert vbo_description, "正しいフォーマットで記述されていません"
                if previous_line_type == DescriptionOfWhat.business_object:
                    vbo_description.description += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.action:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.description += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.pre_condition:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.pre_condition += line_class["content"]
                    continue
                if previous_line_type == DescriptionOfWhat.post_condition:
                    assert latest_action, "正しいフォーマットで記述されていません"
                    latest_action.post_condition += line_class["content"]
                    continue
            if line_class["type"] == "action name":
                assert vbo_description, "正しいフォーマットで記述されていません"
                if latest_action:
                    vbo_description = append_action_to_vbo_description(
                        latest_action, vbo_description
                    )
                latest_action = VBOActionDescription(line_class["content"])
                previous_line_type = DescriptionOfWhat.action
                continue
            if line_class["type"] == "input parameter":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                latest_action.input_params.append(line_class["content"])
                continue
            if line_class["type"] == "output parameter":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                latest_action.output_params.append(line_class["content"])
                continue
            if line_class["type"] == "pre condition":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                previous_line_type = DescriptionOfWhat.pre_condition
                continue
            if line_class["type"] == "post condition":
                assert vbo_description and latest_action, "正しいフォーマットで記述されていません"
                previous_line_type = DescriptionOfWhat.post_condition
                continue
            # debug
            logger.debug("line: {}".format(line.strip()))
            if latest_action:
                logger.debug("latest_action: {}".format(latest_action.as_markdown()))
        else:
            # 最後に残っている latest_action を回収
            if latest_action:
                vbo_description = append_action_to_vbo_description(
                    latest_action, vbo_description
                )

    assert vbo_description, "正しいフォーマットで記述されていません"
    return vbo_description.as_markdown()


if __name__ == "__main__":
    descriptions_folder = Path("output_descriptions")
    for description_file in descriptions_folder.glob("*.txt"):
        with open(descriptions_folder / (description_file.stem + ".md"), "w") as md_f:
            md_f.write(convert_to_markdown(description_file))

```


# 参考URL
* [Is there a way to auto-generate the Business Object Definition (BOD) document.](https://community.blueprism.com/communities/community-home/digestviewer/viewthread?GroupId=139&MID=17850&CommunityKey=ecea0082-1a23-4cd8-ab44-48e94657092e&tab=digestviewer&ReturnUrl=%2Fcommunities%2Fcommunity-home%2Fdigestviewer%3FMessageKey%3D7d81b26b-37c8-429c-99f3-bd24ffbe241e%26CommunityKey%3Decea0082-1a23-4cd8-ab44-48e94657092e%26ReturnUrl%3D%252Fcommunities%252Fcommunity-home%252Fdigestviewer%253FMessageKey%253Da0671d57-27a9-4094-bb1f-31b165670855%2526CommunityKey%253D1b679a41-ef27-4557-a2dd-9af040812654%2526ReturnUrl%253D%25252Fcommunities%25252Fcommunity-home%25252Fdigestviewer%25253FMessageKey%25253D10124247-b4bb-4253-9429-8ca123c6f460%252526CommunityKey%25253D1b679a41-ef27-4557-a2dd-9af040812654)
* [DX: Object Inventory](https://digitalexchange.blueprism.com/dx/entry/3439/solution/object-inventory)