Add unit tests for domain and application layers, and update licenses to correct license
This commit is contained in:
parent
0e03712e61
commit
3e7f6fea8e
29 changed files with 1631 additions and 26 deletions
91
LICENSE_NOTICE.md
Normal file
91
LICENSE_NOTICE.md
Normal file
|
@ -0,0 +1,91 @@
|
|||
# License Notice
|
||||
|
||||
## Sharenet License Information
|
||||
|
||||
This project, Sharenet, is licensed under the **Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License** (CC-BY-NC-SA-4.0).
|
||||
|
||||
## License Details
|
||||
|
||||
- **Full License Text**: See [LICENSE.md](LICENSE.md) for the complete license text
|
||||
- **License URL**: https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
- **SPDX Identifier**: CC-BY-NC-SA-4.0
|
||||
|
||||
## What This License Means
|
||||
|
||||
### You are free to:
|
||||
- **Share**: Copy and redistribute the material in any medium or format
|
||||
- **Adapt**: Remix, transform, and build upon the material
|
||||
- **Attribution**: You must give appropriate credit, provide a link to the license, and indicate if changes were made
|
||||
|
||||
### Under the following terms:
|
||||
- **NonCommercial**: You may not use the material for commercial purposes
|
||||
- **ShareAlike**: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original
|
||||
|
||||
## Copyright Information
|
||||
|
||||
- **Copyright Holder**: Continuist <continuist02@gmail.com>
|
||||
- **Year**: 2024
|
||||
- **Project**: Sharenet
|
||||
|
||||
## License Headers
|
||||
|
||||
All source files in this project include license headers that reference this license. The headers follow this format:
|
||||
|
||||
### For Rust files:
|
||||
```rust
|
||||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
```
|
||||
|
||||
### For TypeScript/JavaScript files:
|
||||
```typescript
|
||||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
```
|
||||
|
||||
## Package Configuration
|
||||
|
||||
### Backend (Rust)
|
||||
- **Cargo.toml**: `license = "CC-BY-NC-SA-4.0"`
|
||||
|
||||
### Frontend (Node.js)
|
||||
- **package.json**: `"license": "CC-BY-NC-SA-4.0"`
|
||||
|
||||
## Compliance
|
||||
|
||||
To comply with this license when using, modifying, or distributing this software:
|
||||
|
||||
1. **Attribution**: Always include the copyright notice and license reference
|
||||
2. **Non-Commercial Use**: Do not use for commercial purposes without permission
|
||||
3. **Share Alike**: If you create derivatives, license them under the same terms
|
||||
4. **License Link**: Include a link to the full license text
|
||||
|
||||
## Questions
|
||||
|
||||
If you have questions about the license or need permission for commercial use, please contact:
|
||||
- **Email**: continuist02@gmail.com
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
This project includes third-party dependencies that are licensed under their own terms. Please refer to the respective license files for each dependency:
|
||||
|
||||
- **Backend Dependencies**: See `backend/Cargo.lock` for dependency licenses
|
||||
- **Frontend Dependencies**: See `frontend/package-lock.json` for dependency licenses
|
||||
|
||||
The CC-BY-NC-SA-4.0 license applies only to the original Sharenet codebase, not to third-party dependencies.
|
415
PROJECT_INDEX.md
Normal file
415
PROJECT_INDEX.md
Normal file
|
@ -0,0 +1,415 @@
|
|||
# Sharenet Project Index
|
||||
|
||||
## Project Overview
|
||||
|
||||
Sharenet is a full-stack application for managing users and products, built with Rust (backend) and Next.js (frontend). It demonstrates clean architecture principles with multiple interface options and configurable storage backends.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend Architecture (Clean Architecture)
|
||||
|
||||
The backend follows clean architecture principles with clear separation of concerns:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Interface Layer │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ API │ │ CLI │ │ TUI │ │
|
||||
│ │ (Axum) │ │ (Clap) │ │ (Ratatui) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Service/UseCase Layer │ │
|
||||
│ │ - UserService │ │
|
||||
│ │ - ProductService │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Domain Layer │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ - User Entity │ │
|
||||
│ │ - Product Entity │ │
|
||||
│ │ - Repository Traits │ │
|
||||
│ │ - Domain Errors │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Memory │ │ PostgreSQL │ │
|
||||
│ │ Repository │ │ Repository │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
sharenet/
|
||||
├── backend/ # Rust backend
|
||||
│ ├── Cargo.toml # Workspace configuration
|
||||
│ ├── README.md # Backend documentation
|
||||
│ ├── crates/ # Backend modules
|
||||
│ │ ├── domain/ # Domain entities and traits
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # Core domain logic
|
||||
│ │ ├── application/ # Application services
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # Use case implementations
|
||||
│ │ ├── api/ # HTTP API (Axum)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # REST API endpoints
|
||||
│ │ ├── cli/ # CLI interface (Clap)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # Command-line interface
|
||||
│ │ ├── tui/ # TUI interface (Ratatui)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # Text user interface
|
||||
│ │ ├── memory/ # In-memory storage
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # Memory repository implementation
|
||||
│ │ ├── postgres/ # PostgreSQL storage
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/lib.rs # PostgreSQL repository implementation
|
||||
│ │ ├── sharenet-api-memory/ # API binary (memory)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/main.rs # API entry point
|
||||
│ │ ├── sharenet-api-postgres/ # API binary (PostgreSQL)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/main.rs # API entry point
|
||||
│ │ ├── sharenet-cli-memory/ # CLI binary (memory)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/main.rs # CLI entry point
|
||||
│ │ ├── sharenet-cli-postgres/ # CLI binary (PostgreSQL)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/main.rs # CLI entry point
|
||||
│ │ ├── sharenet-tui-memory/ # TUI binary (memory)
|
||||
│ │ │ ├── Cargo.toml
|
||||
│ │ │ └── src/main.rs # TUI entry point
|
||||
│ │ └── sharenet-tui-postgres/ # TUI binary (PostgreSQL)
|
||||
│ │ ├── Cargo.toml
|
||||
│ │ └── src/main.rs # TUI entry point
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ │ └── 20240101000000_create_tables.sql
|
||||
│ └── config/ # Configuration files
|
||||
│ ├── api-memory.env # API with memory storage
|
||||
│ ├── api-postgres.env # API with PostgreSQL
|
||||
│ ├── cli-memory.env # CLI with memory storage
|
||||
│ ├── cli-postgres.env # CLI with PostgreSQL
|
||||
│ ├── tui-memory.env # TUI with memory storage
|
||||
│ └── tui-postgres.env # TUI with PostgreSQL
|
||||
├── frontend/ # Next.js frontend
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ ├── README.md # Frontend documentation
|
||||
│ ├── src/ # Source code
|
||||
│ │ ├── app/ # Next.js app directory
|
||||
│ │ │ ├── layout.tsx # Root layout
|
||||
│ │ │ ├── page.tsx # Dashboard page
|
||||
│ │ │ ├── users/ # Users pages
|
||||
│ │ │ │ └── page.tsx # Users management
|
||||
│ │ │ └── products/ # Products pages
|
||||
│ │ │ └── page.tsx # Products management
|
||||
│ │ ├── components/ # React components
|
||||
│ │ │ └── ui/ # shadcn/ui components
|
||||
│ │ │ ├── button.tsx # Button component
|
||||
│ │ │ ├── card.tsx # Card component
|
||||
│ │ │ ├── dialog.tsx # Dialog component
|
||||
│ │ │ ├── form.tsx # Form component
|
||||
│ │ │ ├── input.tsx # Input component
|
||||
│ │ │ ├── label.tsx # Label component
|
||||
│ │ │ └── table.tsx # Table component
|
||||
│ │ └── lib/ # Utility functions
|
||||
│ │ ├── api.ts # API client
|
||||
│ │ └── utils.ts # Utility functions
|
||||
│ ├── public/ # Static assets
|
||||
│ │ ├── file.svg # File icon
|
||||
│ │ ├── globe.svg # Globe icon
|
||||
│ │ ├── next.svg # Next.js logo
|
||||
│ │ ├── vercel.svg # Vercel logo
|
||||
│ │ └── window.svg # Window icon
|
||||
│ ├── components.json # shadcn/ui configuration
|
||||
│ ├── next.config.ts # Next.js configuration
|
||||
│ ├── postcss.config.mjs # PostCSS configuration
|
||||
│ ├── tailwind.config.js # Tailwind CSS configuration
|
||||
│ └── tsconfig.json # TypeScript configuration
|
||||
├── README.md # Project documentation
|
||||
├── LICENSE.md # Project license
|
||||
└── .gitignore # Git ignore rules
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### Domain Layer (`backend/crates/domain/`)
|
||||
|
||||
**Core Entities:**
|
||||
- `User`: User entity with id, username, email, timestamps
|
||||
- `Product`: Product entity with id, name, description, timestamps
|
||||
- `CreateUser`/`UpdateUser`: DTOs for user operations
|
||||
- `CreateProduct`/`UpdateProduct`: DTOs for product operations
|
||||
|
||||
**Traits:**
|
||||
- `Entity`: Base trait for domain entities
|
||||
- `Repository<T>`: Generic repository trait for CRUD operations
|
||||
|
||||
**Error Handling:**
|
||||
- `DomainError`: Domain-specific error types (NotFound, InvalidInput, Internal)
|
||||
|
||||
### Application Layer (`backend/crates/application/`)
|
||||
|
||||
**Services:**
|
||||
- `Service<T, R>`: Generic service implementation
|
||||
- `UseCase<T>`: Use case trait for business operations
|
||||
- `Repository<T>`: Repository trait for data access
|
||||
|
||||
**Features:**
|
||||
- Generic implementations for easy extension
|
||||
- Static dispatch with generic traits
|
||||
- Comprehensive test coverage with mock repositories
|
||||
|
||||
### Infrastructure Layer
|
||||
|
||||
#### Memory Repository (`backend/crates/memory/`)
|
||||
- In-memory storage using `HashMap` and `RwLock`
|
||||
- Suitable for development and testing
|
||||
- No external dependencies
|
||||
|
||||
#### PostgreSQL Repository (`backend/crates/postgres/`)
|
||||
- SQLx-based PostgreSQL implementation
|
||||
- Connection pooling with PgPool
|
||||
- Prepared query caching for performance
|
||||
- Migration support
|
||||
|
||||
### Interface Layer
|
||||
|
||||
#### HTTP API (`backend/crates/api/`)
|
||||
- **Framework**: Axum
|
||||
- **Features**:
|
||||
- RESTful endpoints for users and products
|
||||
- CORS support
|
||||
- Request/response logging
|
||||
- JSON serialization/deserialization
|
||||
- **Endpoints**:
|
||||
- `POST /users` - Create user
|
||||
- `GET /users/:id` - Get user by ID
|
||||
- `GET /users` - List all users
|
||||
- `PUT /users/:id` - Update user
|
||||
- `DELETE /users/:id` - Delete user
|
||||
- `POST /products` - Create product
|
||||
- `GET /products/:id` - Get product by ID
|
||||
- `GET /products` - List all products
|
||||
- `PUT /products/:id` - Update product
|
||||
- `DELETE /products/:id` - Delete product
|
||||
|
||||
#### CLI (`backend/crates/cli/`)
|
||||
- **Framework**: Clap
|
||||
- **Features**:
|
||||
- Subcommand-based interface
|
||||
- User and product management commands
|
||||
- JSON output support
|
||||
- **Commands**:
|
||||
- `user create --username <name> --email <email>`
|
||||
- `user get --id <id>`
|
||||
- `user list`
|
||||
- `user update --id <id> [--username <name>] [--email <email>]`
|
||||
- `user delete --id <id>`
|
||||
- `product create --name <name> --description <desc>`
|
||||
- `product get --id <id>`
|
||||
- `product list`
|
||||
- `product update --id <id> [--name <name>] [--description <desc>]`
|
||||
- `product delete --id <id>`
|
||||
|
||||
#### TUI (`backend/crates/tui/`)
|
||||
- **Framework**: Ratatui
|
||||
- **Features**:
|
||||
- Interactive terminal interface
|
||||
- Table-based data display
|
||||
- Form-based data entry
|
||||
- Navigation between users and products
|
||||
|
||||
### Frontend (`frontend/`)
|
||||
|
||||
#### Technology Stack
|
||||
- **Framework**: Next.js 15.3.3
|
||||
- **Language**: TypeScript
|
||||
- **UI Library**: React 19
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Components**: shadcn/ui
|
||||
- **Form Handling**: React Hook Form with Zod validation
|
||||
- **HTTP Client**: Axios
|
||||
|
||||
#### Key Features
|
||||
- **Dashboard**: Overview page with navigation to users and products
|
||||
- **Users Management**:
|
||||
- View all users in a table
|
||||
- Create new users via dialog
|
||||
- Edit existing users
|
||||
- Delete users with confirmation
|
||||
- **Products Management**:
|
||||
- View all products in a table
|
||||
- Create new products via dialog
|
||||
- Edit existing products
|
||||
- Delete products with confirmation
|
||||
|
||||
#### API Integration
|
||||
- **Base URL**: `http://127.0.0.1:3000`
|
||||
- **Client**: Axios-based API client in `src/lib/api.ts`
|
||||
- **Type Safety**: Full TypeScript interfaces for API responses
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Users Table
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Products Table
|
||||
```sql
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Backend Configuration
|
||||
Configuration files are located in `backend/config/`:
|
||||
- `api-memory.env` - API server with in-memory storage
|
||||
- `api-postgres.env` - API server with PostgreSQL storage
|
||||
- `cli-memory.env` - CLI with in-memory storage
|
||||
- `cli-postgres.env` - CLI with PostgreSQL storage
|
||||
- `tui-memory.env` - TUI with in-memory storage
|
||||
- `tui-postgres.env` - TUI with PostgreSQL storage
|
||||
|
||||
### Environment Variables
|
||||
- `SERVER_ADDR` - Server address (default: 127.0.0.1:3000)
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `RUST_LOG` - Logging level (default: info)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Backend Development
|
||||
1. **Setup**: Install Rust and SQLx CLI
|
||||
2. **Database**: Create database and run migrations
|
||||
3. **Build**: `cargo build`
|
||||
4. **Run**: Choose appropriate binary for your use case
|
||||
5. **Test**: `cargo test`
|
||||
|
||||
### Frontend Development
|
||||
1. **Setup**: Install Node.js and npm
|
||||
2. **Install**: `npm install`
|
||||
3. **Run**: `npm run dev`
|
||||
4. **Build**: `npm run build`
|
||||
|
||||
### Running the Full Stack
|
||||
1. Start backend: `cargo run --bin sharenet-api-postgres`
|
||||
2. Start frontend: `cd frontend && npm run dev`
|
||||
3. Access web interface at `http://localhost:3000`
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Testing
|
||||
- **Unit Tests**: Each crate has comprehensive unit tests
|
||||
- **Integration Tests**: Repository implementations are tested
|
||||
- **Mock Testing**: Mock repositories for isolated testing
|
||||
|
||||
### Frontend Testing
|
||||
- **Component Testing**: Individual React components
|
||||
- **Integration Testing**: API integration and user flows
|
||||
- **E2E Testing**: Full application workflows
|
||||
|
||||
## Deployment
|
||||
|
||||
### Backend Deployment
|
||||
- **Docker**: Containerized deployment
|
||||
- **Binary**: Standalone executables
|
||||
- **Database**: PostgreSQL with connection pooling
|
||||
|
||||
### Frontend Deployment
|
||||
- **Vercel**: Optimized for Next.js
|
||||
- **Static Export**: Can be deployed to any static hosting
|
||||
- **Docker**: Containerized deployment
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Backend
|
||||
- **Connection Pooling**: PostgreSQL connection pooling
|
||||
- **Query Caching**: SQLx prepared query caching
|
||||
- **Async/Await**: Non-blocking I/O operations
|
||||
- **Generic Implementations**: Zero-cost abstractions
|
||||
|
||||
### Frontend
|
||||
- **Code Splitting**: Next.js automatic code splitting
|
||||
- **Static Generation**: Pre-rendered pages where possible
|
||||
- **Optimized Images**: Next.js image optimization
|
||||
- **Bundle Analysis**: Webpack bundle analysis
|
||||
|
||||
## Security
|
||||
|
||||
### Backend Security
|
||||
- **Input Validation**: Domain-level validation
|
||||
- **Error Handling**: Secure error messages
|
||||
- **CORS**: Configurable CORS policies
|
||||
- **SQL Injection**: SQLx prevents SQL injection
|
||||
|
||||
### Frontend Security
|
||||
- **Type Safety**: TypeScript prevents type-related errors
|
||||
- **Input Validation**: Client-side validation with Zod
|
||||
- **XSS Prevention**: React automatic escaping
|
||||
- **CSRF Protection**: Built-in Next.js protection
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- **Authentication**: User authentication and authorization
|
||||
- **Role-Based Access Control**: User roles and permissions
|
||||
- **Product Categories**: Product categorization system
|
||||
- **Inventory Tracking**: Stock management
|
||||
- **Audit Logging**: Operation logging and history
|
||||
- **API Documentation**: OpenAPI/Swagger documentation
|
||||
- **Real-time Updates**: WebSocket support
|
||||
- **File Uploads**: Product image uploads
|
||||
- **Search and Filtering**: Advanced data filtering
|
||||
- **Pagination**: Large dataset handling
|
||||
|
||||
### Technical Improvements
|
||||
- **Caching**: Redis integration for caching
|
||||
- **Monitoring**: Application monitoring and metrics
|
||||
- **Logging**: Structured logging with correlation IDs
|
||||
- **Health Checks**: Application health endpoints
|
||||
- **Rate Limiting**: API rate limiting
|
||||
- **Compression**: Response compression
|
||||
- **CDN**: Static asset delivery optimization
|
||||
|
||||
## Contributing
|
||||
|
||||
### Development Guidelines
|
||||
- **Code Style**: Follow Rust and TypeScript conventions
|
||||
- **Testing**: Maintain high test coverage
|
||||
- **Documentation**: Keep documentation up to date
|
||||
- **Architecture**: Follow clean architecture principles
|
||||
- **Error Handling**: Comprehensive error handling
|
||||
- **Performance**: Consider performance implications
|
||||
|
||||
### Code Review Process
|
||||
- **Pull Requests**: All changes via pull requests
|
||||
- **Code Review**: Mandatory code review
|
||||
- **Testing**: Automated testing on all changes
|
||||
- **Documentation**: Update documentation as needed
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
For more information about the license and compliance requirements, see [LICENSE_NOTICE.md](LICENSE_NOTICE.md).
|
|
@ -144,4 +144,6 @@ The frontend is configured to connect to the backend at `http://127.0.0.1:3000`
|
|||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
For more information about the license and compliance requirements, see [LICENSE_NOTICE.md](LICENSE_NOTICE.md).
|
|
@ -8,7 +8,7 @@ resolver = "2"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Continuist <continuist02@gmail.com>"]
|
||||
license = "CC-BY-NC-4.0"
|
||||
license = "CC-BY-NC-SA-4.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
|
@ -9,3 +9,7 @@ license.workspace = true
|
|||
domain = { path = "../domain" }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
|
||||
chrono = { workspace = true }
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use domain::{Result, Entity};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
@ -76,3 +87,361 @@ impl<T: Entity, R: Repository<T> + Clone> UseCase<T> for Service<T, R> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use domain::{User, CreateUser, UpdateUser, Product, CreateProduct, UpdateProduct};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use chrono::Utc;
|
||||
|
||||
// Mock repository for testing
|
||||
#[derive(Clone)]
|
||||
struct MockRepository<T: Entity> {
|
||||
data: Arc<RwLock<HashMap<Uuid, T>>>,
|
||||
}
|
||||
|
||||
impl<T: Entity + Clone + Send + Sync> MockRepository<T> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository<User> for MockRepository<User> {
|
||||
fn create(&self, data: CreateUser) -> impl Future<Output = Result<User>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
let id = Uuid::new_v4();
|
||||
let user = User {
|
||||
id,
|
||||
username: data.username,
|
||||
email: data.email,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
guard.insert(id, user.clone());
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<User>> + Send {
|
||||
async move {
|
||||
let guard = self.data.read().await;
|
||||
guard.get(&id)
|
||||
.cloned()
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_all(&self) -> impl Future<Output = Result<Vec<User>>> + Send {
|
||||
async move {
|
||||
let guard = self.data.read().await;
|
||||
Ok(guard.values().cloned().collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self, id: Uuid, data: UpdateUser) -> impl Future<Output = Result<User>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
let user = guard.get_mut(&id)
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
|
||||
|
||||
if let Some(username) = data.username {
|
||||
user.username = username;
|
||||
}
|
||||
if let Some(email) = data.email {
|
||||
user.email = email;
|
||||
}
|
||||
user.updated_at = Utc::now();
|
||||
Ok(user.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
guard.remove(&id)
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository<Product> for MockRepository<Product> {
|
||||
fn create(&self, data: CreateProduct) -> impl Future<Output = Result<Product>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
let id = Uuid::new_v4();
|
||||
let product = Product {
|
||||
id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
guard.insert(id, product.clone());
|
||||
Ok(product)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_by_id(&self, id: Uuid) -> impl Future<Output = Result<Product>> + Send {
|
||||
async move {
|
||||
let guard = self.data.read().await;
|
||||
guard.get(&id)
|
||||
.cloned()
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_all(&self) -> impl Future<Output = Result<Vec<Product>>> + Send {
|
||||
async move {
|
||||
let guard = self.data.read().await;
|
||||
Ok(guard.values().cloned().collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self, id: Uuid, data: UpdateProduct) -> impl Future<Output = Result<Product>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
let product = guard.get_mut(&id)
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
|
||||
|
||||
if let Some(name) = data.name {
|
||||
product.name = name;
|
||||
}
|
||||
if let Some(description) = data.description {
|
||||
product.description = description;
|
||||
}
|
||||
product.updated_at = Utc::now();
|
||||
Ok(product.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send {
|
||||
async move {
|
||||
let mut guard = self.data.write().await;
|
||||
guard.remove(&id)
|
||||
.ok_or_else(|| domain::DomainError::NotFound(format!("Entity not found: {}", id)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod service_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service_create() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
let user = service.create(create_user).await.unwrap();
|
||||
assert_eq!(user.username, "test_user");
|
||||
assert_eq!(user.email, "test@example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service_get() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_user).await.unwrap();
|
||||
let found = service.get(created.id).await.unwrap();
|
||||
assert_eq!(found.id, created.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service_list() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let user1 = CreateUser {
|
||||
username: "user1".to_string(),
|
||||
email: "user1@example.com".to_string(),
|
||||
};
|
||||
let user2 = CreateUser {
|
||||
username: "user2".to_string(),
|
||||
email: "user2@example.com".to_string(),
|
||||
};
|
||||
|
||||
service.create(user1).await.unwrap();
|
||||
service.create(user2).await.unwrap();
|
||||
|
||||
let users = service.list().await.unwrap();
|
||||
assert_eq!(users.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service_update() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_user).await.unwrap();
|
||||
let update = UpdateUser {
|
||||
username: Some("updated_user".to_string()),
|
||||
email: None,
|
||||
};
|
||||
|
||||
let updated = service.update(created.id, update).await.unwrap();
|
||||
assert_eq!(updated.username, "updated_user");
|
||||
assert_eq!(updated.email, "test@example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_service_delete() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_user).await.unwrap();
|
||||
service.delete(created.id).await.unwrap();
|
||||
assert!(service.get(created.id).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_product_service_create() {
|
||||
let repo = MockRepository::<Product>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_product = CreateProduct {
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
};
|
||||
|
||||
let product = service.create(create_product).await.unwrap();
|
||||
assert_eq!(product.name, "Test Product");
|
||||
assert_eq!(product.description, "Test Description");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_product_service_get() {
|
||||
let repo = MockRepository::<Product>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_product = CreateProduct {
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_product).await.unwrap();
|
||||
let found = service.get(created.id).await.unwrap();
|
||||
assert_eq!(found.id, created.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_product_service_list() {
|
||||
let repo = MockRepository::<Product>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let product1 = CreateProduct {
|
||||
name: "Product 1".to_string(),
|
||||
description: "Description 1".to_string(),
|
||||
};
|
||||
let product2 = CreateProduct {
|
||||
name: "Product 2".to_string(),
|
||||
description: "Description 2".to_string(),
|
||||
};
|
||||
|
||||
service.create(product1).await.unwrap();
|
||||
service.create(product2).await.unwrap();
|
||||
|
||||
let products = service.list().await.unwrap();
|
||||
assert_eq!(products.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_product_service_update() {
|
||||
let repo = MockRepository::<Product>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_product = CreateProduct {
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_product).await.unwrap();
|
||||
let update = UpdateProduct {
|
||||
name: Some("Updated Product".to_string()),
|
||||
description: None,
|
||||
};
|
||||
|
||||
let updated = service.update(created.id, update).await.unwrap();
|
||||
assert_eq!(updated.name, "Updated Product");
|
||||
assert_eq!(updated.description, "Test Description");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_product_service_delete() {
|
||||
let repo = MockRepository::<Product>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let create_product = CreateProduct {
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
};
|
||||
|
||||
let created = service.create(create_product).await.unwrap();
|
||||
service.delete(created.id).await.unwrap();
|
||||
assert!(service.get(created.id).await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod error_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_found_error() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let result = service.get(Uuid::new_v4()).await;
|
||||
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_nonexistent() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let update = UpdateUser {
|
||||
username: Some("new_username".to_string()),
|
||||
email: None,
|
||||
};
|
||||
|
||||
let result = service.update(Uuid::new_v4(), update).await;
|
||||
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_nonexistent() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let service = Service::new(repo);
|
||||
|
||||
let result = service.delete(Uuid::new_v4()).await;
|
||||
assert!(matches!(result, Err(ApplicationError::Domain(domain::DomainError::NotFound(_)))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use domain::{CreateProduct, CreateUser, Product, User, UpdateProduct, UpdateUser};
|
||||
|
|
|
@ -10,3 +10,6 @@ serde = { workspace = true }
|
|||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "macros", "rt-multi-thread"] }
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
@ -89,17 +100,233 @@ where
|
|||
fn delete(&self, id: Uuid) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
mod user_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
fn test_user_entity_impl() {
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
assert_eq!(user.username, "test_user");
|
||||
assert_eq!(user.email, "test@example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_user_validation() {
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(create_user.username, "test_user");
|
||||
assert_eq!(create_user.email, "test@example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user_partial() {
|
||||
let update_user = UpdateUser {
|
||||
username: Some("new_username".to_string()),
|
||||
email: None,
|
||||
};
|
||||
|
||||
assert_eq!(update_user.username, Some("new_username".to_string()));
|
||||
assert_eq!(update_user.email, None);
|
||||
}
|
||||
}
|
||||
|
||||
mod product_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_product_entity_impl() {
|
||||
let product = Product {
|
||||
id: Uuid::new_v4(),
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
assert_eq!(product.name, "Test Product");
|
||||
assert_eq!(product.description, "Test Description");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_product_validation() {
|
||||
let create_product = CreateProduct {
|
||||
name: "Test Product".to_string(),
|
||||
description: "Test Description".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(create_product.name, "Test Product");
|
||||
assert_eq!(create_product.description, "Test Description");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_product_partial() {
|
||||
let update_product = UpdateProduct {
|
||||
name: Some("New Product Name".to_string()),
|
||||
description: None,
|
||||
};
|
||||
|
||||
assert_eq!(update_product.name, Some("New Product Name".to_string()));
|
||||
assert_eq!(update_product.description, None);
|
||||
}
|
||||
}
|
||||
|
||||
mod domain_error_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_not_found_error() {
|
||||
let error = DomainError::NotFound("User not found".to_string());
|
||||
assert_eq!(error.to_string(), "Entity not found: User not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_input_error() {
|
||||
let error = DomainError::InvalidInput("Invalid email format".to_string());
|
||||
assert_eq!(error.to_string(), "Invalid input: Invalid email format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_internal_error() {
|
||||
let error = DomainError::Internal("Database connection failed".to_string());
|
||||
assert_eq!(error.to_string(), "Internal error: Database connection failed");
|
||||
}
|
||||
}
|
||||
|
||||
mod repository_trait_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Mock implementation of Repository for testing
|
||||
struct MockRepository<T> {
|
||||
data: Arc<RwLock<HashMap<Uuid, T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + Sync> MockRepository<T> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository_create() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let create_user = CreateUser {
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
};
|
||||
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: create_user.username.clone(),
|
||||
email: create_user.email.clone(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
repo.data.write().await.insert(user.id, user.clone());
|
||||
let guard = repo.data.read().await;
|
||||
let stored = guard.get(&user.id).unwrap();
|
||||
assert_eq!(stored.username, create_user.username);
|
||||
assert_eq!(stored.email, create_user.email);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository_find_by_id() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
repo.data.write().await.insert(user.id, user.clone());
|
||||
let guard = repo.data.read().await;
|
||||
let found = guard.get(&user.id).unwrap();
|
||||
assert_eq!(found.id, user.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository_find_all() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let user1 = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "user1".to_string(),
|
||||
email: "user1@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
let user2 = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "user2".to_string(),
|
||||
email: "user2@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
repo.data.write().await.insert(user1.id, user1.clone());
|
||||
repo.data.write().await.insert(user2.id, user2.clone());
|
||||
|
||||
let all = repo.data.read().await;
|
||||
assert_eq!(all.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository_update() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
repo.data.write().await.insert(user.id, user.clone());
|
||||
let mut guard = repo.data.write().await;
|
||||
let stored = guard.get_mut(&user.id).unwrap();
|
||||
stored.username = "updated_user".to_string();
|
||||
|
||||
drop(guard);
|
||||
let read_guard = repo.data.read().await;
|
||||
let updated = read_guard.get(&user.id).unwrap();
|
||||
assert_eq!(updated.username, "updated_user");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_repository_delete() {
|
||||
let repo = MockRepository::<User>::new();
|
||||
let user = User {
|
||||
id: Uuid::new_v4(),
|
||||
username: "test_user".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
repo.data.write().await.insert(user.id, user.clone());
|
||||
repo.data.write().await.remove(&user.id);
|
||||
assert!(repo.data.read().await.get(&user.id).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
@ -142,20 +153,5 @@ impl Repository<Product> for InMemoryProductRepository {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
|
||||
pub type MemoryUserService = Service<User, InMemoryUserRepository>;
|
||||
pub type MemoryProductService = Service<Product, InMemoryProductRepository>;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET\n username = COALESCE($1, username),\n email = COALESCE($2, email),\n updated_at = CURRENT_TIMESTAMP\n WHERE id = $3\n RETURNING id, username, email, created_at, updated_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "508d8e56fc561537060a4bf378dc057cc55999d2115521419d00ca57843af046"
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, username, email, created_at, updated_at\n FROM users\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "54394890f1b4efd6487c5147e9a1942a86d7b2edfeea1cfb432758acbfbe6da6"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, username, email, created_at, updated_at\n FROM users\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5bb95e6a9bea1ac6d4aab08eb63b1f9127a3a6b95046bb91bea60a2d6ee6dfc2"
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, name, description, created_at, updated_at\n FROM products\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "62874c01dcd85e441608d6d92661277e3b3638412204a2aa5fad7a51c3ce855d"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM products\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "82746e921edfb33ae6c0f434d96ce74e0de9dc8e24ae1d9fa945b9924498a652"
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO products (name, description)\n VALUES ($1, $2)\n RETURNING id, name, description, created_at, updated_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "882fbe26ce3c2f8baf74b89805fc4be0d99874bee7ffed9d948fc57759f03d61"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM users\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169"
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE products\n SET\n name = COALESCE($1, name),\n description = COALESCE($2, description),\n updated_at = CURRENT_TIMESTAMP\n WHERE id = $3\n RETURNING id, name, description, created_at, updated_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c424e1240a945e7dea975c325a1a7db21d5503ccfb9f9ad788f3897fb8216eee"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, name, description, created_at, updated_at\n FROM products\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ed0fa11d45b99c47dcbf75aa0646cf91516f91388d484e398273be3532d8e027"
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (username, email)\n VALUES ($1, $2)\n RETURNING id, username, email, created_at, updated_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ffa2228196655e9f2b8152fc4b79d720cadc68f2d12bed4f46aeb87401ce2de9"
|
||||
}
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/*
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "CC-BY-NC-SA-4.0",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
@ -69,7 +80,7 @@ export default function ProductsPage() {
|
|||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const handleDelete = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this product?')) {
|
||||
try {
|
||||
await productApi.delete(id);
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* This file is part of Sharenet.
|
||||
*
|
||||
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
*
|
||||
* You may obtain a copy of the license at:
|
||||
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*
|
||||
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'http://127.0.0.1:3000';
|
||||
|
|
Loading…
Add table
Reference in a new issue