はじめに
前回までの続きです。
今回はGoogleCalendarAPIを参考に、カレンダーの取得APIを実装します
実装箇所
GoogleCalendarAPIのカレンダーの取得を実施していきます!
GoogleCalendarAPIの仕様
→赤枠の部分を実装します。では次にレスポンスを見ていきましょう!
{
"kind": "calendar#calendarListEntry", // このリソースが calendarListEntry であることを示す固定値
"etag": "\"p1234567890abcdef\"", // リソースのバージョン識別子(変更検知用)
"id": "alice@example.com", // カレンダーのID(主カレンダーの場合はメールアドレス)
"summary": "Alice's Work Calendar", // カレンダーの名称(デフォルト名)
"description": "カンファレンスや締め切りなどの予定を管理するカレンダー", // 説明文
"location": "Tokyo Headquarters", // カレンダーの物理的な場所(任意)
"timeZone": "Asia/Tokyo", // このカレンダーのタイムゾーン
"summaryOverride": "Work", // 特定ユーザーにとっての表示名(上書き名)
"colorId": "9", // カレンダーの色ID(Googleが定義するプリセット)
"backgroundColor": "#9fc6e7", // 背景色(hex値)
"foregroundColor": "#000000", // 文字色(hex値)
"hidden": false, // このユーザーにとって非表示かどうか
"selected": true, // UIで選択されているか(表示対象か)
"accessRole": "owner", // このユーザーのカレンダーに対する権限(owner, writer, reader など)
"defaultReminders": [ // デフォルトのリマインダー設定(通知方法と何分前か)
{
"method": "popup", // 通知方法(popup, emailなど)
"minutes": 10 // イベントの何分前に通知するか
},
{
"method": "email",
"minutes": 30
}
],
"notificationSettings": { // 通知の種類と方法
"notifications": [
{
"type": "eventCreation", // 通知トリガーの種類(イベント作成など)
"method": "email" // 通知方法(email, sms, popupなど)
},
{
"type": "eventChange",
"method": "popup"
}
]
},
"primary": true, // このユーザーにとって主カレンダーか(true は削除不可)
"deleted": false, // 削除済みカレンダーか(差分同期時にのみ true が返る)
"conferenceProperties": {
"allowedConferenceSolutionTypes": [ // 許可されている会議ツールの種類
"hangoutsMeet", // Google Meet 会議の許可
"eventHangout" // 従来のHangout
]
}
}
→こんな感じですね。さすがGoogleですね!!!レスポンスを見るだけでも汎用性高く、尚且つパフォーマンスを高めているんですね!!十分すぎるので、ちょっと最低限にレスポンスを変更していこうと思います。
{
"id": 1, // カレンダーのID
"summary": "Alice's Work Calendar", // カレンダーの名称(デフォルト名)
"description": "カンファレンスや締め切りなどの予定を管理するカレンダー", // 説明文
"summaryOverride": "Work", // 特定ユーザーにとっての表示名(上書き名)
"color": "blue", // カレンダーの色
"hidden": false, // このユーザーにとって非表示かどうか
"selected": true, // UIで選択されているか(表示対象か)
"accessRole": "owner", // このユーザーのカレンダーに対する権限(owner, writer, reader など)
"defaultReminders": [ // デフォルトのリマインダー設定(通知方法と何分前か)
{
"method": "popup", // 通知方法(popup, emailなど)
"minutes": 10 // イベントの何分前に通知するか
},
{
"method": "email",
"minutes": 30
}
],
"notificationSettings": { // 通知の種類と方法
"notifications": [
{
"type": "eventCreation", // 通知トリガーの種類(イベント作成など)
"method": "email" // 通知方法(email, sms, popupなど)
},
{
"type": "eventChange",
"method": "popup"
}
]
},
"primary": true, // このユーザーにとって主カレンダーか(true は削除不可)
"deleted": false, // 削除済みカレンダーか(差分同期時にのみ true が返る)
"conferenceProperties": {
"allowedConferenceSolutionTypes": [ // 許可されている会議ツールの種類
"hangoutsMeet", // Google Meet 会議の許可
"eventHangout" // 従来のHangout
]
}
}
→いまのテーブル構成では対応できない部分があるので、追加でSQLを流します。
ALTER TABLE calendar_memberships
ADD COLUMN summary_override varchar,
ADD COLUMN hidden boolean DEFAULT false,
ADD COLUMN selected boolean DEFAULT true,
ADD COLUMN is_primary boolean DEFAULT false,
ADD COLUMN is_deleted boolean DEFAULT false;
ALTER TABLE calendar_memberships DROP COLUMN IF EXISTS summary_override;
成果物
GET
http://localhost:8081/calendars?user_id=1
・response
[
{
"id": 1,
"summary": "Alice Work",
"description": "Work calendar for Alice",
"color": "blue",
"visibility": "default",
"hidden": false,
"selected": true,
"accessRole": "owner",
"defaultReminders": [
{
"method": "popup",
"minutes": 10
}
],
"notificationSettings": {
"notifications": null
},
"primary": true,
"deleted": false,
"conferenceProperties": {
"allowedConferenceSolutionTypes": [
"hangoutsMeet",
"eventHangout"
]
}
},
{
"id": 2,
"summary": "Bob Personal",
"description": "Personal calendar for Bob",
"color": "green",
"visibility": "private",
"hidden": false,
"selected": true,
"accessRole": "owner",
"defaultReminders": [
{
"method": "popup",
"minutes": 10
}
],
"notificationSettings": {
"notifications": null
},
"primary": true,
"deleted": false,
"conferenceProperties": {
"allowedConferenceSolutionTypes": [
"hangoutsMeet",
"eventHangout"
]
}
},
{
"id": 3,
"summary": "Team Project",
"description": "カンファレンスや締切用",
"color": "blue",
"visibility": "default",
"hidden": false,
"selected": true,
"accessRole": "owner",
"defaultReminders": [
{
"method": "popup",
"minutes": 10
}
],
"notificationSettings": {
"notifications": null
},
"primary": true,
"deleted": false,
"conferenceProperties": {
"allowedConferenceSolutionTypes": [
"hangoutsMeet",
"eventHangout"
]
}
}
]
ソースコード
~/develop/google_calendar_sample (feat/echo_bun)$ tree -I pgdata/
.
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── init
│ ├── 01_schema.sql
│ ├── 02_seed.sql
│ └── 03_alter_table.sql
├── internal
│ ├── controller
│ │ └── calendar_controller.go
│ ├── model
│ │ ├── calendar.go
│ │ └── user_settings.go
│ ├── repository
│ │ ├── calendar_repository.go
│ │ └── user_settings_repository.go
│ └── service
│ └── calendar_service.go
main.go
package main
import (
"database/sql"
"fmt"
"log"
"os"
"calendar/internal/controller"
"calendar/internal/repository"
"calendar/internal/service"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
_ "github.com/uptrace/bun/driver/pgdriver"
)
func main() {
// Initialize Echo
e := echo.New()
// Database connection
dsn := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_NAME"),
)
sqldb, err := sql.Open("pg", dsn)
if err != nil {
log.Fatal(err)
}
db := bun.NewDB(sqldb, pgdialect.New())
// Test database connection
if err := db.Ping(); err != nil {
log.Fatal(err)
}
// Initialize repositories
calendarRepo := repository.NewCalendarRepository(db)
settingsRepo := repository.NewUserSettingsRepository(db)
// Initialize service
calendarService := service.NewCalendarService(calendarRepo, settingsRepo)
// Initialize controller
calendarController := controller.NewCalendarController(calendarService)
// Register routes
calendarController.RegisterRoutes(e)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
internal/controller/calendar_controller.go
package controller
import (
"net/http"
"calendar/internal/controller/dto"
"calendar/internal/model"
"calendar/internal/service"
"github.com/labstack/echo/v4"
)
type CalendarController struct {
service service.CalendarService
}
func NewCalendarController(service service.CalendarService) *CalendarController {
return &CalendarController{service: service}
}
func (c *CalendarController) RegisterRoutes(e *echo.Echo) {
e.GET("/calendars", c.ListCalendars)
}
func (c *CalendarController) ListCalendars(ctx echo.Context) error {
// リクエストパラメータのバインディング
req := new(dto.ListCalendarsRequest)
if err := ctx.Bind(req); err != nil {
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request parameters"})
}
// バリデーション
if req.UserID == 0 {
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "user_id is required"})
}
// サービス層の呼び出し
calendars, err := c.service.ListCalendars(ctx.Request().Context(), req.UserID)
if err != nil {
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
// レスポンスの変換
response := make([]dto.CalendarResponse, len(calendars))
for i, calendar := range calendars {
response[i] = convertToCalendarResponse(calendar)
}
return ctx.JSON(http.StatusOK, response)
}
// convertToCalendarResponse converts a model.Calendar to CalendarResponse
func convertToCalendarResponse(calendar *model.Calendar) dto.CalendarResponse {
reminders := make([]dto.ReminderResponse, len(calendar.DefaultReminders))
for i, r := range calendar.DefaultReminders {
reminders[i] = dto.ReminderResponse{
Method: r.Method,
Minutes: r.Minutes,
}
}
notifications := make([]dto.NotificationResponse, len(calendar.NotificationSettings.Notifications))
for i, n := range calendar.NotificationSettings.Notifications {
notifications[i] = dto.NotificationResponse{
Type: n.Type,
Method: n.Method,
Enabled: n.Enabled,
}
}
return dto.CalendarResponse{
ID: calendar.ID,
Summary: calendar.Summary,
Description: calendar.Description,
Color: calendar.Color,
Visibility: calendar.Visibility,
SummaryOverride: calendar.SummaryOverride,
Hidden: calendar.Hidden,
Selected: calendar.Selected,
AccessRole: calendar.AccessRole,
DefaultReminders: reminders,
NotificationSettings: dto.NotificationSettingsResponse{Notifications: notifications},
Primary: calendar.Primary,
Deleted: calendar.Deleted,
ConferenceProperties: dto.ConferenceResponse{
AllowedConferenceSolutionTypes: calendar.ConferenceProperties.AllowedConferenceSolutionTypes,
},
}
}
internal/controller/dto/calendar.go
package dto
// CalendarResponse represents the response structure for calendar data
type CalendarResponse struct {
ID int64 `json:"id"`
Summary string `json:"summary"`
Description string `json:"description"`
Color string `json:"color"`
Visibility string `json:"visibility"`
SummaryOverride string `json:"summaryOverride,omitempty"`
Hidden bool `json:"hidden"`
Selected bool `json:"selected"`
AccessRole string `json:"accessRole"`
DefaultReminders []ReminderResponse `json:"defaultReminders"`
NotificationSettings NotificationSettingsResponse `json:"notificationSettings"`
Primary bool `json:"primary"`
Deleted bool `json:"deleted"`
ConferenceProperties ConferenceResponse `json:"conferenceProperties"`
}
type ReminderResponse struct {
Method string `json:"method"`
Minutes int `json:"minutes"`
}
type NotificationResponse struct {
Type string `json:"type"`
Method string `json:"method"`
Enabled bool `json:"enabled"`
}
type NotificationSettingsResponse struct {
Notifications []NotificationResponse `json:"notifications"`
}
type ConferenceResponse struct {
AllowedConferenceSolutionTypes []string `json:"allowedConferenceSolutionTypes"`
}
// ListCalendarsRequest represents the request parameters for listing calendars
type ListCalendarsRequest struct {
UserID int64 `query:"user_id" validate:"required"`
}
internal/service/calendar_service.go
package service
import (
"context"
"calendar/internal/model"
"calendar/internal/repository"
)
type CalendarService interface {
ListCalendars(ctx context.Context, userID int64) ([]*model.Calendar, error)
}
type calendarService struct {
calendarRepo repository.CalendarRepository
settingsRepo repository.UserSettingsRepository
}
func NewCalendarService(
calendarRepo repository.CalendarRepository,
settingsRepo repository.UserSettingsRepository,
) CalendarService {
return &calendarService{
calendarRepo: calendarRepo,
settingsRepo: settingsRepo,
}
}
func (s *calendarService) ListCalendars(ctx context.Context, userID int64) ([]*model.Calendar, error) {
calendars, err := s.calendarRepo.List(ctx)
if err != nil {
return nil, err
}
// ユーザー設定の取得
settings, err := s.settingsRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
// レスポンスデータの整形
for _, calendar := range calendars {
// ユーザー設定からデフォルトのリマインダー設定を取得
reminders := make([]model.Reminder, 0)
for method, minutes := range settings.DefaultReminders {
reminders = append(reminders, model.Reminder{
Method: method,
Minutes: minutes,
})
}
calendar.DefaultReminders = reminders
// ユーザー設定から通知設定を取得
calendar.NotificationSettings = settings.NotificationSettings
// 会議プロパティ
calendar.ConferenceProperties = model.ConferenceProperties{
AllowedConferenceSolutionTypes: []string{"hangoutsMeet", "eventHangout"},
}
// その他のデフォルト値
calendar.Hidden = false
calendar.Selected = true
calendar.AccessRole = "owner"
calendar.Primary = true
calendar.Deleted = false
}
return calendars, nil
}
internal/repository/calendar_repository.go
package repository
import (
"context"
"calendar/internal/model"
"github.com/uptrace/bun"
)
type CalendarRepository interface {
List(ctx context.Context) ([]*model.Calendar, error)
}
type calendarRepository struct {
db *bun.DB
}
func NewCalendarRepository(db *bun.DB) CalendarRepository {
return &calendarRepository{db: db}
}
func (r *calendarRepository) List(ctx context.Context) ([]*model.Calendar, error) {
var calendars []*model.Calendar
err := r.db.NewSelect().Model(&calendars).Scan(ctx)
return calendars, err
}
internal/repository/user_settings_repository.go
package repository
import (
"context"
"calendar/internal/model"
"github.com/uptrace/bun"
)
type UserSettingsRepository interface {
GetByUserID(ctx context.Context, userID int64) (*model.UserSettings, error)
}
type userSettingsRepository struct {
db *bun.DB
}
func NewUserSettingsRepository(db *bun.DB) UserSettingsRepository {
return &userSettingsRepository{db: db}
}
func (r *userSettingsRepository) GetByUserID(ctx context.Context, userID int64) (*model.UserSettings, error) {
settings := new(model.UserSettings)
err := r.db.NewSelect().
Model(settings).
Where("user_id = ?", userID).
Scan(ctx)
return settings, err
}
internal/model/calendar.go
package model
import (
"time"
"github.com/uptrace/bun"
)
type Reminder struct {
Method string `json:"method"`
Minutes int `json:"minutes"`
}
type Notification struct {
Type string `json:"type"`
Method string `json:"method"`
Enabled bool `json:"enabled"`
}
type NotificationSettings struct {
Notifications []Notification `json:"notifications"`
}
type ConferenceProperties struct {
AllowedConferenceSolutionTypes []string `json:"allowedConferenceSolutionTypes"`
}
type Calendar struct {
bun.BaseModel `bun:"table:calendars"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
OwnerID int64 `bun:"owner_id,notnull" json:"-"`
Summary string `bun:"summary,notnull" json:"summary"`
Description string `bun:"description" json:"description"`
Color string `bun:"color" json:"color"`
Visibility string `bun:"visibility" json:"visibility"`
SummaryOverride string `bun:"-" json:"summaryOverride,omitempty"`
Hidden bool `bun:"-" json:"hidden"`
Selected bool `bun:"-" json:"selected"`
AccessRole string `bun:"-" json:"accessRole"`
DefaultReminders []Reminder `bun:"-" json:"defaultReminders"`
NotificationSettings NotificationSettings `bun:"-" json:"notificationSettings"`
Primary bool `bun:"-" json:"primary"`
Deleted bool `bun:"-" json:"deleted"`
ConferenceProperties ConferenceProperties `bun:"-" json:"conferenceProperties"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"-"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"-"`
}
internal/model/user_settings.go
package model
import (
"time"
"github.com/uptrace/bun"
)
type UserSettings struct {
bun.BaseModel `bun:"table:user_settings"`
ID int64 `bun:"id,pk,autoincrement" json:"-"`
UserID int64 `bun:"user_id,notnull" json:"-"`
DefaultReminders map[string]int `bun:"default_reminders,type:jsonb" json:"defaultReminders"`
WorkingHours []byte `bun:"working_hours,type:jsonb" json:"-"`
ColorTheme string `bun:"color_theme" json:"-"`
NotificationSettings NotificationSettings `bun:"notification_settings,type:jsonb" json:"notificationSettings"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"-"`
}