ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

JavaScriptにTypeScriptの型チェックを導入してみる

TypeScript: Handbook - Type Checking JavaScript Files
このようなサイトを見つけたので試してみました。公式なので当然かもしれませんが、上記のサイトに書いてあるようなJavaScriptファイルにTypeScriptの型チェックをできました。

以下、やったことのメモ書きです。

前提条件

Win10 + Visual Studio CodeJavaScript(TypeScript)の実行環境はVisual Studio CodeのRemote Containersとしました。ですが、Remote Containers以外はOS、エディタに関わらずできるはずです。

環境構築

JavaScriptの実行環境として「vscode-remote-try-node」をgit cloneし、Visual Studio CodeのRemote Containersで起動します。この時点でコンテナにはnode、npmがインストールされており、JavaScirptがデバッグ実行できるはずです。

次にTypeScriptを導入するために、以下のコマンドを実行します。

$ npm install -g typescript
$ tsc --init

tsc --init」で「tsconfig.json」が作成されますが、今回はJavaScriptに対してtscでのチェックを行いたいので、ファイル名を「jsconfig.json」に変更します。JavaScriptに対応させるため「jsconfig.json」を以下のように変更しました。(コメントは変更した理由など)

{
  "compilerOptions": {
(中略)
    "checkJs": true, // JavaScirptのチェックを行う
(中略)
    "noImplicitAny": false, // メソッドの引数でany型を指定していない場合のエラーを回避する
(中略)

tscコマンドを実行するか、Visual Studio CodeならTypeScript用のプラグインを入れることで、型チェックが行われて(エラーがあれば)適切なエラーが表示されると思います。

とりあえず最初に紹介した公式のサンプルソースで試してみましたが、「this.constructorOnly」の型エラーは表示されることを確認しました。またメソッドに引数を追加してみたところany型を指定していない旨のエラーとなったので、これについては表示されないようjsconfig.jsonを修正しています。

色んなパターンのJavaScriptのプログラムを試したわけではありませんが、一応、TypeScriptの型チェックがJavaScriptファイルに対してできたみたいです。

localstack と Visual Studio Code の Remote-Containers でAWSの開発環境を構築してみる

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!
「localstack」というAWSを仮想的にローカルマシン内のDockerで動かすものがあります。

この「localstack」で起動した仮想のAWSに S3 バケットを作り、Visual Studio Code の Remote-Containers のGolangのコードから取得できる環境をつくってみました。

localstack、Remote-Containers を使うメリットは以下があると考えています。

  • locakstackを使うことで実際のAWSに触る必要がない(つまり料金もかからない、credentialsの後述のとおりダミーで可能)
  • Remote-Containers を使うことで開発言語毎の環境をローカルにインストールする必要がなくなる

今回はWindows 10 Pro、Dockerはインストール済、AWS CLIについてはローカルのWindowsにインストール済という状態で始めました。
以下、今回やったことの手順となります。

1. localstackの起動、S3バケットの用意

AWSを仮想的に動かすため、localstackを起動します。とはいっても git clone して Dockerで起動するだけです。以下になります。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose u

起動したら(ローカルの) AWS CLIにlocalstackに接続するためのcredentialsを定義します。ここに記載するcredentialsのアクセスキー・シークレットキーはダミーで大丈夫なので、今回は「dummy」という値としました。以下をAWSの「credentials」ファイルに記載します。

[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy

プロファイル名は「localstack」、アクセスキーとシークレットキーは先述の通りダミーの値です。この定義を使い、AWS CLIバケットを作成してみます。

$ aws s3 mb s3://sample-bucket --endpoint-url=http://localhost:4566 --profile=localstack
$ aws s3 ls --endpoint-url=http://localhost:4566 --profile=localstack

2. Remote-Containers

Golangの開発環境を用意し、先に作ったバケットにアクセスしてみます。開発環境の用意はいくつかやり方があるかと思いますが、今回は以下の公式のサンプルを git clone して、ソースを書き替えました。
https://github.com/microsoft/vscode-remote-try-go

$ git clone https://github.com/microsoft/vscode-remote-try-go

vscode-remote-try-go」をVisual Studio Codeで開き、Remote-Containersでコンテナとして開くと、デバッグ実行や「go run」コマンドでソースの実行ができるはずです。

今回は先に用意したlocalstackの(仮想の)AWS環境にアクセスするため、「Dockerfile」に以下を追記して環境変数を追加しました。

ENV AWS_ACCESS_KEY_ID dummy
ENV AWS_SECRET_ACCESS_KEY dummy
ENV AWS_ENDPOINT "http://host.docker.internal:4566"

最初の2行はAWSのアクセスキーとシークレットキーですが、こちらもダミーの値で大丈夫です。これらがないと後述のGolangのソースを実行するときに「NoCredentialProviders: no valid providers in chain. Deprecated.」というエラーとなり、AWSにアクセスできなくなるので注意してください。
3行目はlocalstackで起動したローカルのAWSに接続するためのエンドポイントです。ポートはlocalstackで起動したコンテナのものとなり、先にAWS CLIバケットを用意したときのエンドポイントと同じとなります。「host.docker.internal」ですが、これはGolangを実行するコンテナ内からホストマシンを参照すすためのものです。「localhost」とか書いてしまうと、Golangを実行するコンテナ自身を指してしまい、「localstack」のコンテナを見ることができなくなります。

最後にGolangのソースです。AWS SDK Goを使い、バケットの一覧を取得しています。今回は「main.go」という名前にしました。

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

func main() {
	sess, err := session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region:   aws.String("ap-northeast-1"),
			Endpoint: aws.String(os.Getenv("AWS_ENDPOINT")),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	svc := s3.New(sess)
	result, err := svc.ListBuckets(nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result)
}

awsのConfigの作成で、環境変数の「AWS_ENDPOINT」を参照しているのが分かるかと思います。実案件では「AWS_ENDPOINT」があれば使い、なければ使わないというような処理になるかと思います。
実行すると以下のようにAWS CLIで作成したバケット名が見れると思います。

$ go run main.go
{
  Buckets: [{
      CreationDate: 2020-10-02 09:57:33.988551 +0000 UTC,
      Name: "sample-bucket"
    }],
  (中略)
}

Windows 10 で Visual Studio Code の Remote development in Containers を試してみた

タイトルにあるように、WindowsVisual Studio Code Remote を試してみました。意図としては、開発環境ごとにコンテナを用意し、ローカルにいろいろ入れるのは止めたい、というのが希望です。

以下、その時の作業の備忘録です。

事前準備

Developing inside a Container using Visual Studio Code Remote Development
こちらにあるように、Windowsの場合は

をあらかじめインストールしておく必要があるようです。(WSL2が必要なのは意外でした。裏で使っているようですが・・・)

デバッグ実行できるかを試す

事前準備にてインストールしたら
Get started with development Containers in Visual Studio Code
の公式のチュートリアルをやってみました。

一点、チュートリアルでは「vscode-remote-try-node」をVSCode上から落としてきていますが、今回はgithubからgit cloneでローカルのフォルダに直接ダウンロードしました。

ダウンロードしたフォルダをVSCodeで開くと「Folder contains a Dev Container configuration file. Reopen folder to develop in a container (learn more).・・・」というダイアログが右下に表示されるので「Reopen in Container」を押下します。プロジェクト内に「.devcontainer/devcontainer.json」があることによりコンテナ内での開発環境だと判断されるためでしょう。

後はメインのプログラムである「server.js」の適当な箇所にブレークポイントを置き、「F5」を押下すると起動し、ブラウザからアクセスすることでデバッグできることを確認できました。

同じような流れでGolangのサンプルである「vscode-remote-try-go」もローカルにgit cloneしてデバッグを試してみましたが、こちらも「F5」で起動してデバッグすることができました。

まとめ

コンテナでのリモート開発を行うサンプルの「vscode-remote-try-~」はnodeやGolang以外にもJavaPythonなど良く使いそうなものはあるみたいです。まっさらな環境から特定言語の動作環境を作るのは意外と面倒 & ローカル環境が汚れると感じることも多いのですが、開発を行う環境ごとにコンテナを立てれば、そういった懸念も少なるかもしれません。

追記

VSCodeとDockerでMacにGolangの開発環境を作成する | Developers.IO

こちらで紹介されている手順だとコマンドパレットから直接、作りたい環境を指定してコンテナ環境を作ることができそうです。(サンプルではないですが)

aws-sdk-goのMockを使ったテスト

Mocking Techniques for Go. Go provides you all the tools you need… | by kyleyost | CBI Engineering | Jun, 2020 | Medium

こちらの記事を読んでいたところ、以下のような記述がありました。

When working with the aws-sdk, they provide interfaces for all of their major services

(https://medium.com/cbi-engineering/mocking-techniques-for-go-805c10f1676b より)

上記の記事ではDynamoDBを使う場合のテストについて書いてありますが
自分は最近S3を使ったので、S3のinterfaceを使ったテストを書いてみました。

以下、ソースについてです。

ソース

「ListObjectsPages」を使い、指定された「フォルダ」内の.logファイルを取得するメソッドと、そのテストです。
(厳密にはS3には「フォルダ」はないですが、まあサンプルということで・・・)

メソッドを実装した「logfile.go」と、テストの「logfile_test.go」から成ります。

logfile.go

package logfile

import (
	"errors"
	"fmt"
	"path"
	"path/filepath"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
)

// FindLogFiles ...
func FindLogFiles(svc s3iface.S3API, bucket, folder string) ([]string, error) {
	fmt.Printf("find logFiles start. srcBucket = %s, folder = %s\n", bucket, folder)

	var logFiles []string
	params := &s3.ListObjectsInput{
		Bucket: aws.String(bucket),
		Prefix: aws.String(folder),
	}

	err := svc.ListObjectsPages(params, func(p *s3.ListObjectsOutput, last bool) (shouldContinue bool) {
		for _, obj := range p.Contents {
			fileName := path.Base(*obj.Key)
			if filepath.Ext(fileName) != ".log" {
				continue
			}
			logFiles = append(logFiles, *obj.Key)
		}
		return true
	})
	if err != nil {
		return nil, errors.New("failed to list objects")
	}

	fmt.Println("find logFiles complete.")
	return logFiles, nil
}

logfile_test.go

package logfile

import (
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
)

type mockS3Client struct {
	s3iface.S3API
	objects []*s3.Object
}

func (m *mockS3Client) ListObjectsPages(input *s3.ListObjectsInput, fn func(p *s3.ListObjectsOutput, last bool) bool) error {
	output := &s3.ListObjectsOutput{}
	output.Contents = m.objects
	last := false
	fn(output, last)
	return nil
}

func TestFindLogFiles(t *testing.T) {
	test := func(objects []*s3.Object, expectCount int) {
		svc := &mockS3Client{objects: objects}
		logFiles, err := FindLogFiles(svc, "sample-bucket", "log-folder/sample-folder")
		if err != nil {
			t.Fatalf("error raise. %#v", err)
		}
		if len(logFiles) != expectCount {
			t.Fatalf("not expect logs, result count = %v\n", len(logFiles))
		}
		fmt.Printf("logFiles = %v\n", logFiles)
	}

	objects := []*s3.Object{}
	objects = append(objects, &s3.Object{Key: aws.String("log_test1.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test2.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test3.log")})
	objects = append(objects, &s3.Object{Key: aws.String("log_test4.err")})
	test(objects, 3)
}

FindLogFiles()ではS3のサービスのオブジェクトを「s3iface.S3API」インターフェース型の引数として受け取り、そのメソッドを呼び出して一覧を取得するようにしています。
テストではこの「s3iface.S3API」インターフェースを満たすMockを作り(「mockS3Client」)、これをFindLogFiles()に渡すことで実際にS3を呼び出さずにテストをパスするようにしています。

NuxtJSのことはじめ

タイトルにあるようにNuxtJSを始めてみました。
Visual Studio Codeでいくつか入門ページをやってみましたが、以下を覚えておくと便利そうだと思ったのでメモ代わりに残しておきます。

NuxtJSのプロジェクト作成~起動

以下のコマンドで作成します。(NuxtJSはインストール済である前提で)

$ npx create-nuxt-app your-favorite-project

インストール時に選択肢が出てきますが、今回は以下のように選択してみました。

? Project name: your-favorite-project
? Programming language: TypeScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code)                           
  • modulesには「Axios」を選択
  • Linting toolsには「ESLint」

を選びました。(Rendaring modeや言語は適宜変えることになるかと思います。)

起動は以下となります。

$ npm run dev

Visual Studio Code の 設定

上記でLinting toolsに「ESLint」を選んだため、ソースを保存するたびにESListのチェックが入ります。
手動でフォーマットを変更するのは面倒なので、プロジェクトフォルダ内のVisual Studio Code の 設定ファイルに以下の記述を行いました。

.vscode/settings.json

{
    "editor.tabSize": 2,
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
    }
}

Visual Studio CodeのESListプラグインはインストールしてある前提です。
見ての通り、タブのインデントの指定と、ソース保存時にESLintのフォーマットを行っています。

まとめ

必要最小限のコマンドや設定ですが、上記を覚えておくことでストレスなくHello World的なものは進められると思います。
また便利な設定などあれば追記していくつもりです。

Golangで2つのgzファイルを連結してみる

gzファイルはコマンドだと以下のように連結することができます。

cat sample1.gz sample2.gz > result.gz

これと同じようにgzファイルを連結する処理をGolangで書いてみました。
検索すれば似たようなことをやっている記事は沢山出てくるかと思いますが、備忘録替わりに載せておきます。

ソースについて

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。
今回はファイルの中身をjsonとしたのですが、特にjson形式を解析して何かを行っているわけではないので、中身の形式は問わないと思います。

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"
)

func write(zr1 *gzip.Reader, zr2 *gzip.Reader) error {
	writeFile, err := os.OpenFile("./result.json.gz", os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer writeFile.Close()

	zw := gzip.NewWriter(writeFile)
	defer zw.Close()

	_, err = io.Copy(zw, zr1)
	if err != nil {
		return err
	}

	_, err = zw.Write([]byte("\n"))
	if err != nil {
		return err
	}

	_, err = io.Copy(zw, zr2)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	readFile1, err := os.Open("./sample1.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile1.Close()

	zr1, err := gzip.NewReader(readFile1)
	if err != nil {
		log.Fatal(err)
	}
	defer zr1.Close()

	readFile2, err := os.Open("./sample2.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile2.Close()

	zr2, err := gzip.NewReader(readFile2)
	if err != nil {
		log.Fatal(err)
	}
	defer zr2.Close()

	err = write(zr1, zr2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("-----finish-----")
}

簡単にやっていることを書くと、main()で行っていることは以下となります。

  • 2つのjsonを読み込むReader(zr1、zr2)を作成する
  • 2つのReaderをwrite()メソッドに渡して、中身を連結する

となります。

write()では以下の流れでに2つのReaderの中身を連結します。

  • 最初に結果を「result.json.gz」として出力するためのWriter(zw)を作成する
  • io.Copy()を使い、1つ目のReader(zr1)から読み込んだ内容をWriter(zw)に出力する
  • zw.Write()で改行を出力する
  • io.Copy()を使い、2つ目のReader(zr2)から読み込んだ内容をWriter(zw)に出力する


以上になりますが、io.Copy()を使ってio.Readerをio.Writerにコピーしているところや、io.WriterのWrite()で改行を書き込んでいるところなどは
Golangのio周りを意識するのにちょうど良かったように感じました。

Golangで構造体を定義せずにjsonを読み込んでみる

ふとした時にGolangjsonを読み込む話になり
事前に構造体を定義しないでreflectで読み込んで云々・・・みたいな話になりました。

そう言えばGolangでreflectをまともに使ったことがないなあ、と思い
勉強がてらサンプルソースを作ってみました。

以下、サンプルソースとその機能について、備忘録替わりに書いておきます。

機能について

サポートされるログと検出されるフィールド - Amazon CloudWatch Logs

こちらにある CloudWatch のjson形式のログを読み込むことにしました。
最初に書いたようにjsonに対応する構造体は定義せず、reflectを使いjsonを読み込みます。
ネストした任意のjsonを読み込み、キー・値・型を取得する機能を想定しています。

実行には上記のjsonを「sample.json」という名前で保存しておく必要があります。

サンプルプログラム

以下のようになりました。

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"reflect"
)

func doReflect(interfc interface{}) {
	r := reflect.ValueOf(interfc)

	if r.Kind() == reflect.Map {
		iter := r.MapRange()
		for iter.Next() {
			k := iter.Key()
			v := iter.Value()
			_, isStr := v.Interface().(string)
			_, isNum := v.Interface().(float64)
			if isStr || isNum {
				fmt.Printf("keys = %v, val = %v, type = %v\n", k, v, reflect.TypeOf(v.Interface()))
			} else {
				doReflect(v.Interface())
			}

		}
		return
	} else if r.Kind() == reflect.Slice {
		for i := 0; i < r.Len(); i++ {
			doReflect(r.Index(i).Interface())
		}
	}
}

func main() {
	bytes, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Fatal(err)
	}

	var contents interface{}
	err = json.Unmarshal(bytes, &contents)
	if err != nil {
		log.Fatal(err)
	}

	doReflect(contents)
}

実行すると、jsonのキー・値・型が出力されるかと思います。
型は今回はstring、floatのみですが、jsonによっては型を増やすことも可能かと思います。