diff --git a/main.go b/main.go index 44c32b3f..0a072be4 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ const ( ParcelStatusDelivered = "delivered" ) +// Parcel представляет информацию о посылке. type Parcel struct { Number int Client int @@ -22,14 +23,17 @@ type Parcel struct { CreatedAt string } +// ParcelService представляет сервис для работы с посылками. type ParcelService struct { store ParcelStore } +// NewParcelService создает экземпляр ParcelService. func NewParcelService(store ParcelStore) ParcelService { return ParcelService{store: store} } +// Register регистрирует новую посылку. func (s ParcelService) Register(client int, address string) (Parcel, error) { parcel := Parcel{ Client: client, @@ -51,22 +55,23 @@ func (s ParcelService) Register(client int, address string) (Parcel, error) { return parcel, nil } +// PrintClientParcels выводит список посылок клиента. func (s ParcelService) PrintClientParcels(client int) error { parcels, err := s.store.GetByClient(client) if err != nil { return err } - fmt.Printf("Посылки клиента %d:\n", client) + fmt.Printf("Посылки клиента №%d:\n", client) for _, parcel := range parcels { fmt.Printf("Посылка № %d на адрес %s от клиента с идентификатором %d зарегистрирована %s, статус %s\n", parcel.Number, parcel.Address, parcel.Client, parcel.CreatedAt, parcel.Status) + fmt.Println() } - fmt.Println() - return nil } +// NextStatus обновляет статус посылки на следующий. func (s ParcelService) NextStatus(number int) error { parcel, err := s.store.Get(number) if err != nil { @@ -88,21 +93,30 @@ func (s ParcelService) NextStatus(number int) error { return s.store.SetStatus(number, nextStatus) } +// ChangeAddress обновляет адрес посылки. func (s ParcelService) ChangeAddress(number int, address string) error { return s.store.SetAddress(number, address) } +// Delete удаляет посылку. func (s ParcelService) Delete(number int) error { return s.store.Delete(number) } func main() { - // настройте подключение к БД + // Открываем соединение с базой данных. + db, err := sql.Open("sqlite", "tracker.db") + if err != nil { + fmt.Println(err) + return + } + defer db.Close() - store := // создайте объект ParcelStore функцией NewParcelStore + // Создаём сервис для работы с посылками. + store := NewParcelStore(db) service := NewParcelService(store) - // регистрация посылки + // Регистрируем новую посылку. client := 1 address := "Псков, д. Пушкина, ул. Колотушкина, д. 5" p, err := service.Register(client, address) @@ -111,7 +125,7 @@ func main() { return } - // изменение адреса + // Меняем адрес посылки. newAddress := "Саратов, д. Верхние Зори, ул. Козлова, д. 25" err = service.ChangeAddress(p.Number, newAddress) if err != nil { @@ -119,51 +133,49 @@ func main() { return } - // изменение статуса + // Меняем статус посылки. err = service.NextStatus(p.Number) if err != nil { fmt.Println(err) return } - // вывод посылок клиента + // Выводим список посылок клиента. err = service.PrintClientParcels(client) if err != nil { fmt.Println(err) return } - // попытка удаления отправленной посылки + // Пытаемся удалить отправленную посылку. err = service.Delete(p.Number) if err != nil { fmt.Println(err) - return } - // вывод посылок клиента - // предыдущая посылка не должна удалиться, т.к. её статус НЕ «зарегистрирована» + // Вывод посылок клиента. + // Предыдущая посылка не должна удалиться, т.к. её статус НЕ «зарегистрирована». err = service.PrintClientParcels(client) if err != nil { fmt.Println(err) return } - // регистрация новой посылки + // Регистрируем новую посылку. p, err = service.Register(client, address) if err != nil { fmt.Println(err) return } - // удаление новой посылки + // Удаляем новую посылку. err = service.Delete(p.Number) if err != nil { fmt.Println(err) - return } - // вывод посылок клиента - // здесь не должно быть последней посылки, т.к. она должна была успешно удалиться + // Вывод посылок клиента. + // Здесь не должно быть последней посылки, т.к. она должна была успешно удалиться. err = service.PrintClientParcels(client) if err != nil { fmt.Println(err) diff --git a/parcel.go b/parcel.go index db6c815d..b3dea121 100644 --- a/parcel.go +++ b/parcel.go @@ -2,59 +2,115 @@ package main import ( "database/sql" + "errors" ) +// ParcelStore представляет хранилище посылок. type ParcelStore struct { db *sql.DB } +// NewParcelStore создает экземпляр ParcelStore. func NewParcelStore(db *sql.DB) ParcelStore { return ParcelStore{db: db} } +// Add создает новую посылку в БД. func (s ParcelStore) Add(p Parcel) (int, error) { - // реализуйте добавление строки в таблицу parcel, используйте данные из переменной p - - // верните идентификатор последней добавленной записи - return 0, nil + res, err := s.db.Exec("INSERT INTO parcel (client, status, address, created_at) VALUES (@client, @status, @address, @created_at)", + sql.Named("client", p.Client), + sql.Named("status", p.Status), + sql.Named("address", p.Address), + sql.Named("created_at", p.CreatedAt)) + if err != nil { + return 0, err + } + id, err := res.LastInsertId() + if err != nil { + return 0, err + } + return int(id), nil } +// Get возвращает информацию о посылке по её номеру. func (s ParcelStore) Get(number int) (Parcel, error) { - // реализуйте чтение строки по заданному number - // здесь из таблицы должна вернуться только одна строка - - // заполните объект Parcel данными из таблицы p := Parcel{} - + err := s.db.QueryRow("SELECT number, client, status, address, created_at FROM parcel WHERE number = @number", + sql.Named("number", number)).Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return p, errors.New("parcel not found") + } + return p, err + } return p, nil } +// GetByClient возвращает список посылок клиента. func (s ParcelStore) GetByClient(client int) ([]Parcel, error) { - // реализуйте чтение строк из таблицы parcel по заданному client - // здесь из таблицы может вернуться несколько строк - - // заполните срез Parcel данными из таблицы - var res []Parcel - - return res, nil + rows, err := s.db.Query("SELECT number, client, status, address, created_at FROM parcel WHERE client = @client", + sql.Named("client", client)) + if err != nil { + return nil, err + } + defer rows.Close() + + var ps []Parcel + for rows.Next() { + p := Parcel{} + err := rows.Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt) + if err != nil { + return nil, err + } + ps = append(ps, p) + } + if err := rows.Err(); err != nil { + return nil, err + } + return ps, nil } +// SetStatus обновляет статус посылки. func (s ParcelStore) SetStatus(number int, status string) error { - // реализуйте обновление статуса в таблице parcel - - return nil + _, err := s.db.Exec("UPDATE parcel SET status = @status WHERE number = @number", + sql.Named("status", status), + sql.Named("number", number)) + return err } +// SetAddress обновляет адрес посылки только если статус 'registered'. func (s ParcelStore) SetAddress(number int, address string) error { - // реализуйте обновление адреса в таблице parcel - // менять адрес можно только если значение статуса registered - + res, err := s.db.Exec("UPDATE parcel SET address = @address WHERE number = @number AND status = @status", + sql.Named("address", address), + sql.Named("number", number), + sql.Named("status", ParcelStatusRegistered)) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errors.New("parcel already sent") + } return nil } +// Delete удаляет посылку только если статус 'registered'. func (s ParcelStore) Delete(number int) error { - // реализуйте удаление строки из таблицы parcel - // удалять строку можно только если значение статуса registered - + res, err := s.db.Exec("DELETE FROM parcel WHERE number = @number AND status = @status", + sql.Named("number", number), + sql.Named("status", ParcelStatusRegistered)) + if err != nil { + return err + } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return errors.New("parcel already sent") + } return nil } diff --git a/parcel_test.go b/parcel_test.go index d1b93827..31599445 100644 --- a/parcel_test.go +++ b/parcel_test.go @@ -7,82 +7,116 @@ import ( "time" "github.com/stretchr/testify/require" + _ "modernc.org/sqlite" ) -var ( - // randSource источник псевдо случайных чисел. - // Для повышения уникальности в качестве seed - // используется текущее время в unix формате (в виде числа) - randSource = rand.NewSource(time.Now().UnixNano()) - // randRange использует randSource для генерации случайных чисел - randRange = rand.New(randSource) -) +// RandSource источник псевдо случайных чисел. +var randSource = rand.NewSource(time.Now().UnixNano()) -// getTestParcel возвращает тестовую посылку +// getTestParcel возвращает тестовую посылку. func getTestParcel() Parcel { return Parcel{ - Client: 1000, + Client: int(randSource.Int63()), Status: ParcelStatusRegistered, - Address: "test", + Address: "test address", CreatedAt: time.Now().UTC().Format(time.RFC3339), } } -// TestAddGetDelete проверяет добавление, получение и удаление посылки +// TestAddGetDelete тестирует добавление, получение и удаление посылки. func TestAddGetDelete(t *testing.T) { - // prepare - db, err := // настройте подключение к БД - store := NewParcelStore(db) - parcel := getTestParcel() - - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // get - // получите только что добавленную посылку, убедитесь в отсутствии ошибки - // проверьте, что значения всех полей в полученном объекте совпадают со значениями полей в переменной parcel + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() - // delete - // удалите добавленную посылку, убедитесь в отсутствии ошибки - // проверьте, что посылку больше нельзя получить из БД + store := NewParcelStore(db) + p := getTestParcel() + + // Добавление посылки. + id, err := store.Add(p) + require.NoError(t, err) + require.NotZero(t, id) + + // Получение посылки. + stored, err := store.Get(id) + require.NoError(t, err) + p.Number = id // Устанавливаем Number для корректного сравнения + require.Equal(t, p, stored) + + // Удаление посылки. + err = store.Delete(id) + require.NoError(t, err) + + // Проверка удаления. + _, err = store.Get(id) + require.Error(t, err) } -// TestSetAddress проверяет обновление адреса +// TestSetAddress тестирует обновление адреса посылки. func TestSetAddress(t *testing.T) { - // prepare - db, err := // настройте подключение к БД - - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // set address - // обновите адрес, убедитесь в отсутствии ошибки - newAddress := "new test address" + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() - // check - // получите добавленную посылку и убедитесь, что адрес обновился + store := NewParcelStore(db) + p := getTestParcel() + + // Добавление посылки. + id, err := store.Add(p) + require.NoError(t, err) + require.NotZero(t, id) + + // Обновление адреса. + newAddr := "new test address" + err = store.SetAddress(id, newAddr) + require.NoError(t, err) + + // Проверка обновления. + stored, err := store.Get(id) + require.NoError(t, err) + require.Equal(t, newAddr, stored.Address) + + // Обновление статуса. + err = store.SetStatus(id, ParcelStatusSent) + require.NoError(t, err) + + // Попытка обновления адреса отправленной посылки. + err = store.SetAddress(id, newAddr) + require.Error(t, err) } -// TestSetStatus проверяет обновление статуса +// TestSetStatus тестирует обновление статуса посылки. func TestSetStatus(t *testing.T) { - // prepare - db, err := // настройте подключение к БД + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + + store := NewParcelStore(db) + p := getTestParcel() - // add - // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора + // Добавление посылки. + id, err := store.Add(p) + require.NoError(t, err) + require.NotZero(t, id) - // set status - // обновите статус, убедитесь в отсутствии ошибки + // Обновление статуса. + err = store.SetStatus(id, ParcelStatusSent) + require.NoError(t, err) - // check - // получите добавленную посылку и убедитесь, что статус обновился + // Проверка обновления. + stored, err := store.Get(id) + require.NoError(t, err) + require.Equal(t, ParcelStatusSent, stored.Status) } -// TestGetByClient проверяет получение посылок по идентификатору клиента +// TestGetByClient тестирует получение списка посылок клиента. func TestGetByClient(t *testing.T) { - // prepare - db, err := // настройте подключение к БД + // Подключение к базе данных. + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + // Создание тестовых посылок. parcels := []Parcel{ getTestParcel(), getTestParcel(), @@ -90,32 +124,30 @@ func TestGetByClient(t *testing.T) { } parcelMap := map[int]Parcel{} - // задаём всем посылкам один и тот же идентификатор клиента - client := randRange.Intn(10_000_000) - parcels[0].Client = client - parcels[1].Client = client - parcels[2].Client = client + // Присваиваем всем посылкам одинаковый идентификатор клиента. + client := rand.Intn(10_000_000) + for i := range parcels { + parcels[i].Client = client + } - // add + // Добавление посылок в базу данных. for i := 0; i < len(parcels); i++ { - id, err := // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - - // обновляем идентификатор добавленной у посылки + id, err := NewParcelStore(db).Add(parcels[i]) + require.NoError(t, err) + require.NotZero(t, id) parcels[i].Number = id - - // сохраняем добавленную посылку в структуру map, чтобы её можно было легко достать по идентификатору посылки parcelMap[id] = parcels[i] } - // get by client - storedParcels, err := // получите список посылок по идентификатору клиента, сохранённого в переменной client - // убедитесь в отсутствии ошибки - // убедитесь, что количество полученных посылок совпадает с количеством добавленных + // Получение списка посылок клиента. + got, err := NewParcelStore(db).GetByClient(client) + require.NoError(t, err) + require.Len(t, got, len(parcels)) - // check - for _, parcel := range storedParcels { - // в parcelMap лежат добавленные посылки, ключ - идентификатор посылки, значение - сама посылка - // убедитесь, что все посылки из storedParcels есть в parcelMap - // убедитесь, что значения полей полученных посылок заполнены верно + // Проверка содержимого списка по идентификатору посылки. + for _, p := range got { + want, ok := parcelMap[p.Number] + require.True(t, ok) + require.Equal(t, want, p) } } diff --git a/tracker.db b/tracker.db index b6ba48a1..4939e2d5 100644 Binary files a/tracker.db and b/tracker.db differ