もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

Vue+VuetifyでVueI18nを使ってみた

はじめに

ここ最近は、個人的な勉強でVue.js+Vuetifyを使うことが多い

Vue.js楽しい

Androidアプリの作成やらSpringBoot + Thymeleafを使ってきた影響で、外部ファイルに文字列を定義するクセがついた

いわゆるi18n対応

Vue.js+Vuetifyでi18n対応するためには、VueI18nというライブラリを使用する。

ただ、参考サイトの通りにしても(したつもりのだけか?)上手くいかなかった・・・

上手くできた環境・設定やら、ちょっと工夫したことをメモ書き

環境

  • typescript: 4.1.5
  • vue: 2.6.11
  • vuetify: 2.4.0
  • vue-i18n: 8.24.1

とりあえず使用できるところまで

参考サイト

kazupon.github.io

vuetifyjs.com

Vue CLIで作ったプロジェクトディレクトリで以下のコマンドを実行し、VueI18nライブラリを追加

$ npm install vue-i18n

テキストを取り出す外部ファイルを追加

// src/i18n/ja.ts
import ja from 'vuetify/src/locale/ja'

export const ja = {
  ...ja,

  sample1: 'Hello sample!!!!!',
  sample2: 'こんにちは',
  sample3: 'つよくなりたい'
}

Vuetifyで外部ファイルから読み込むコードを追加

// src/plugins/vuetify.ts
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
import VueI18n from 'vue-i18n'
import ja from '@/i18n/ja'

Vue.use(Vuetify)
Vue.use(VueI18n)

const messages = {
  ja: ja
}

export default new Vuetify({
  lang: {
    current: 'ja',
    locales: messages
  }
})

Typescriptの設定を追加

{
  "compilerOptions": {
    // (中略)
    "types": [
      "webpack-env",
      "vuetify"
    ],
    // (中略)
}

Viewで呼び出す

<p v-text="$vuetify.lang.t('$vuetify.sample1')"/>
<p v-text="$vuetify.lang.t('$vuetify.sample2')"/>
<p v-text="$vuetify.lang.t('$vuetify.sample3')"/>

実際の表示

f:id:monakaice88:20210321191941p:plain

もっと短く書けるように

「$vuetify」を2回記述しないといけないし、もっと短く書けるようにしたい

Vueのプラグイン機能を利用して、短く書けるようにした

参考サイト

stackoverflow.com

github.com

blog.asial.co.jp

Vuetifyのコードを元に戻しておく

// src/plugins/vuetify.ts
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'

Vue.use(Vuetify)

export default new Vuetify({
})

新しいプラグインを作成

// src/plugins/myText.ts
import { PluginObject } from 'vue/types/plugin'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import ja from '@/i18n/ja'

// Typescriptでエラーにならないようにするために追加
// @see https://github.com/vuejs/vue/issues/5641
declare module 'vue/types/vue' {
  interface Vue {
    $myText: (key: string, ...params: Array<string | number>) => string;
  }
}

Vue.use(VueI18n)

const i18n = new VueI18n({
  locale: 'ja',
  messages: {
    ja: ja
  }
})

export const MyText: PluginObject<void> = {
  install (vue: typeof Vue): void {
    // 統合開発環境では、どこでも使用されていないことになっているが、Vueで使用しているので削除しないこと
    vue.prototype.$myText = (key: string, ...params: Array<string | number>): string => {
      const t = i18n.t(key, params)
      if (typeof t === 'string') {
        return t
      }
      throw new Error('not found key.')
    }
  }
}

新しく作成したプラグインを使用するために、設定を追記

// src/main.ts
import Vue from 'vue'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
import { MyText } from '@/plugins/myText'

Vue.use(MyText)

Vue.config.productionTip = false

new Vue({
  vuetify,
  render: h => h(App)
}).$mount('#app')

Viewで実際に使ってみる

<p v-text="$myText('sample3')"/>
<p v-text="$myText('sample2')"/>
<p v-text="$myText('sample1')"/>

実際の表示

f:id:monakaice88:20210321191906p:plain

Go言語でZipの圧縮・解凍を実装してみた

はじめに

少しでも安全にファイル・ディレクトリのやり取りを行うためのツールを作ることにした。

ファイル・ディレクトリを圧縮→暗号化したバイナリファイルを何処かに送りつけ、受け取りは復号→圧縮データを解凍みたいな。

Window・Macの両方でツールを使用したいので、Go言語で作ることにした。

先立って圧縮処理を作ることにした。

軽く調べたところ、Zip方式だとそれなりにサンプルがあったので、Go言語でZipの圧縮・解凍処理を書いてみた。

環境

  • 開発OS:Windows 10
  • Go:1.12.7

参考サイト

圧縮のソースコードは以下のサイトを参考にした。

stackoverflow.com

解凍は以下のサイト(多分)

meideru.com

その他参考サイト

golang.org

ハマったところ

参考サイトをそのままコピペで動かしたところ色々問題があった・・・(特に圧縮側)

  • ファイル単体の圧縮に対応出来なかったり、ディレクトリの文字列の最後に「\」が無いと正しく動かない。
  • 空のディレクトリを含んでいるディレクトリを圧縮したところ、空のディレクトリがZipの中に無い。
  • Go言語の実行ファイルで圧縮は出来るが、解凍が出来ない。(フォルダを作ろうとしてくれない。ただし7zipでは解凍できる。)
  • メタ情報(ファイル・ディレクトリの更新日時など)がZipに含まれていない。

とりあえずチマチマ修正して、自分が使う上での問題点は解決出来た。

ソースコード

Go言語でZipの圧縮・解凍は面倒臭いです。(白目)

圧縮

package zip

import (
    "../logger"
    "archive/zip"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path/filepath"
)

func Compress(writer io.Writer, target string) error {
    zipWriter := zip.NewWriter(writer)
    defer func() {
        if err := zipWriter.Close(); err != nil {
            logger.Errorln(err.Error())
        }
    }()

    isDir, err := isDirectory(target)
    if err != nil {
        return err
    }

    if isDir {
        if err := addZipFiles(zipWriter, target, ""); err != nil {
            return err
        }
    } else {
        fileName := filepath.Base(target)
        if err := addZipFile(zipWriter, target, fileName); err != nil {
            return err
        }
    }

    return nil
}

func isDirectory(path string) (bool, error) {
    fileInfo, err := os.Stat(path)
    if err != nil {
        return false, &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not access directory: '%s'", path),
        }
    }
    return fileInfo.IsDir(), nil
}

func addZipFiles(writer *zip.Writer, basePath, pathInZip string) error {
    fileInfoArray, err := ioutil.ReadDir(basePath)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not read directory: '%s'", basePath),
        }
    }

    basePath = complementPath(basePath)
    pathInZip = complementPath(pathInZip)

    for _, fileInfo := range fileInfoArray {
        newBasePath := basePath + fileInfo.Name()
        newPathInZip := pathInZip + fileInfo.Name()

        if fileInfo.IsDir() {
            if err = addDirectory(writer, newBasePath); err != nil {
                return err
            }

            newBasePath = newBasePath + string(os.PathSeparator)
            newPathInZip = newPathInZip + string(os.PathSeparator)
            if err = addZipFiles(writer, newBasePath, newPathInZip); err != nil {
                return err
            }
        } else {
            if err = addZipFile(writer, newBasePath, newPathInZip); err != nil {
                return err
            }
        }
    }

    return nil
}

func addZipFile(writer *zip.Writer, targetFilePath, pathInZip string) error {
    logger.Infoln("Target file:" + targetFilePath)
    logger.Infoln("Path in zip:" + pathInZip)

    data, err := ioutil.ReadFile(targetFilePath)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not read file: '%s'", targetFilePath),
        }
    }

    fileInfo, err := os.Lstat(targetFilePath)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Fail read header: '%s'", targetFilePath),
        }
    }

    header, err := zip.FileInfoHeader(fileInfo)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Fail create file info header: '%s'", targetFilePath),
        }
    }

    header.Name = pathInZip
    header.Method = zip.Deflate
    w, err := writer.CreateHeader(header)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not create zip: '%s'", targetFilePath),
        }
    }
    _, err = w.Write(data)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not write zip: '%s'", targetFilePath),
        }
    }
    return nil
}

func addDirectory(writer *zip.Writer, basePath string) error {
    fileInfo, err := os.Lstat(basePath)
    if err != nil {
        return err
    }
    header, err := zip.FileInfoHeader(fileInfo)
    if err != nil {
        return err
    }
    if _, err = writer.CreateHeader(header); err != nil {
        return err
    }
    return nil
}

func complementPath(path string) string {
    l := len(path)
    if l == 0 {
        return path
    }

    lastChar := path[l-1 : l]
    if lastChar == "/" || lastChar == "\\" {
        return path
    } else {
        return path + string(os.PathSeparator)
    }
}

解凍

※圧縮と解凍処理は同じファイルに書いているため、import文は省略

func Decompress(dest, target string) error {
    reader, err := zip.OpenReader(target)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not read file: '%s'", target),
        }
    }
    defer func() {
        _ = reader.Close()
    }()

    for _, zippedFile := range reader.File {
        path := filepath.Join(dest, zippedFile.Name)
        if zippedFile.FileInfo().IsDir() {
            err = os.MkdirAll(path, zippedFile.Mode())
            logger.Infoln("created directory: " + path)
            if err != nil {
                return &Error{
                    internalError: err,
                    Message:       fmt.Sprintf("Can not create directory: '%s'", path),
                }
            }
        } else {
            logger.Infoln("zipped file name: " + zippedFile.Name)
            if err = createFileFromZipped(path, zippedFile); err != nil {
                return err
            }
        }
    }

    return nil
}

func createFileFromZipped(path string, zippedFile *zip.File) error {
    reader, err := zippedFile.Open()
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not open zipped file: '%s'", zippedFile.Name),
        }
    }
    defer func() {
        _ = reader.Close()
    }()

    destFile, err := os.Create(path)
    if err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Can not open file: '%s'", path),
        }
    }
    defer func() {
        _ = destFile.Close()
    }()

    if _, err := io.Copy(destFile, reader); err != nil {
        return &Error{
            internalError: err,
            Message:       fmt.Sprintf("Failed copy data: '%s'", path),
        }
    }

    return nil
}

例外

例外はコンナンでええやろ(適当)

package zip

type Error struct {
    internalError error
    Message       string
}

func (z *Error) Error() string {
    var output = z.Message
    if z.internalError != nil {
        output = output + "\n\t" + z.internalError.Error()
    }
    return output
}

実際に使ってみる

とりあえずZipの圧縮・解凍をする実行ファイルを作ってみた。

CLIコマンドとして使えるように、「urfave/cli」を利用してみた。

「urfave/cli」について、参考にしたサイト↓↓↓

qiita.com

package main

import (
    "../../internal/logger"
    "../../internal/zip"
    "bytes"
    "github.com/urfave/cli"
    "os"
)

func main() {
    app := cli.NewApp()

    app.Name = "zip"
    app.Version = "0.0.1"

    app.Action = defaultAction

    app.Commands = []*cli.Command{
        {
            Name:   "c",
            Usage:  "create zip archive",
            Action: zipProcess,
        },
        {
            Name:   "d",
            Usage:  "unzip archive",
            Action: unzipProcess,
        },
    }

    err := app.Run(os.Args)
    if err != nil {
        logger.Error(err)
    }
}

func defaultAction(context *cli.Context) error {
    return cli.ShowAppHelp(context)
}

func zipProcess(context *cli.Context) error {
    var buffer bytes.Buffer
    filePath := context.Args().Get(0)
    if _, err := os.Stat(filePath); err != nil {
        return err
    }

    logger.Infoln("Target file path: " + filePath)
    logger.Infoln("Start zip")
    if err := zip.Compress(&buffer, filePath); err != nil {
        return err
    }
    logger.Infoln("Complete zip")

    file, err := os.Create("./test.zip")
    if err != nil {
        return err
    }
    defer func() {
        _ = file.Close()
    }()

    _, err = file.Write(buffer.Bytes())
    if err != nil {
        return err
    }

    return nil
}

func unzipProcess(context *cli.Context) error {
    targetFilePath := context.Args().Get(0)
    if _, err := os.Stat(targetFilePath); err != nil {
        return err
    }

    logger.Infoln("Target file path: " + targetFilePath)

    logger.Infoln("Unzipping...")
    err := zip.Decompress("./unzipped", targetFilePath)
    if err != nil {
        return err
    }
    logger.Infoln("Unzipped")

    return nil
}

以下のようなコマンドを実際にZipの解凍・圧縮が出来るようになった。

圧縮(実行ファイルがzip.exeの場合)

zip.exe c <ファイル・ディレクトリパス>

解凍

zip.exe d <Zipファイルパス>

おわりに

圧縮時にはメタ情報(ファイルの更新日時のような)をZipに含めるようにした。

けれども解凍時には、そのメタ情報を無視している。(ディレクトリの生成に使っているぐらい)

完全に別のファイルになるし、更新日時とか引き継がなくても良いかなと・・・

Kotlinで期待していない例外UndeclaredThrowableExceptionがやってくる

はじめに

SpringBoot + Kotlinでテストを書いていた

自分で作成した例外が投げられるのを期待したテストを書いていたけど、期待していた例外が発生しなかった・・・

発生していた例外は「UndeclaredThrowableException」

Undeclared・・・未宣言、、、一体なんのこと??

SpringBoot + Kotlinでのコーディングは初めてだったのもあり解決にちょっと時間が掛かった

環境

  • SpringBoot:2.2.2
  • Kotlin:1.3.61

結論

結局、例外が発生するメソッドに「@Throws」を付ければ解決した

@Service
class CompanyService(
        private val companyRepository: CompanyRepository
) {

    @Transactional
    @Throws(NotFoundException::class)  // <---コレを追加
    fun deleteCompany(id: Int) {
        val company = companyRepository.findById(id)
                .orElseThrow {
                    NotFoundException()
                }
        companyRepository.delete(company)
        companyRepository.flush()
    }

}

実際のユニットテスト

@Test(expected = NotFoundException::class)
fun deleteCompany_NotFound_001() {
    run {
        val company = companyRepository.findById(/*存在しないデータ*/999999)
        Assert.assertFalse(company.isPresent)
    }

    target.deleteCompany(999999)
}

例外の定義

@ResponseStatus(value = HttpStatus.NOT_FOUND)
class NotFoundException: Exception() {
}

@Throwsを付けていない場合「target.deleteCompany(999999)」を呼び出すと、期待しているNotFoundExceptionではなく、UndeclaredThrowableExceptionが発生しテストが失敗する

発生したUndeclaredThrowableExceptionインスタンスの内部に、実際に投げている例外インスタンス(ここで言うNotFoundException)があった

@ResponseStatusを付与しているし、UndeclaredThrowableExceptionをハンドルせずに投げっぱなしにしたいなぁと思っていた

@Throwsがあったのを思い出して、試しに付けてみたら期待した動作になった

おわりに

Kotlinのメソッド内でJavaで言うところの検査例外を投げ、その例外が@Throwsで宣言していない場合、UndeclaredThrowableExceptionにラッパーされるっぽい

@Throwsを書かなくてもKotlinはコンパイルエラーにならないので、Spring(SpringBoot)の場合、@ControllerAdviceとか@ExceptionHandlerでUndeclaredThrowableExceptionをハンドリングするのが楽なのかもしれない(でもこの方法使えるのControllerだけか・・・?)

参考サイト

stackoverflow.com

docs.oracle.com

gsh-kz.hatenadiary.org