From: Aidan Cornelius-Bell Date: Sun, 15 Sep 2024 06:54:27 +0000 (+0930) Subject: v2 API X-Git-Url: https://gitweb.mndrdr.org/?a=commitdiff_plain;h=d2e45a489f6db8a80c47e6e31fa135da2f1e3f5d;p=arelpe.git v2 API --- diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..f709764 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,89 @@ +# API Documentation + +## Base URL +`http://localhost:3000/api/v2` + +## Authentication +All API requests require an API key to be included in the header: +``` +X-API-Key: your_api_key +``` + +## Endpoints + +### Posts + +#### List Posts +- **GET** `/posts` +- **Query Parameters:** + - `page`: Page number (default: 1) + - `per_page`: Items per page (default: 15) + - `filter`: Filter posts by type ('posts', 'bookmarks', or 'all') +- **Response:** JSON array of post objects + +#### Get a Specific Post +- **GET** `/posts/:id` +- **Response:** JSON object of the requested post + +#### Create a New Post +- **POST** `/posts` +- **Request Body:** + ```json + { + "data": { + "type": "posts", + "attributes": { + "post_type": "dispatch" or "bookmark", + "title": "Post Title", + "content": "Post content (required for dispatch)", + "url": "https://example.com (required for bookmark)", + "published_at": "2024-09-15T12:00:00Z", + "tags": "tag1, tag2" + } + } + } + ``` +- **Response:** JSON object of the created post + +#### Update a Post +- **PATCH** `/posts/:id` +- **Request Body:** Same as Create, but only include fields to be updated +- **Response:** JSON object of the updated post + +#### Delete a Post +- **DELETE** `/posts/:id` +- **Response:** Empty response with status 204 No Content + +### Pages + +#### List Pages +- **GET** `/pages` +- **Response:** JSON array of page objects + +#### Get a Specific Page +- **GET** `/pages/:slug` +- **Response:** JSON object of the requested page + +## Error Handling +- All errors return appropriate HTTP status codes and a JSON object with error details. +- Common error codes: 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 422 (Unprocessable Entity) + +## Data Models + +### Post +- `id`: Integer +- `post_type`: String ("dispatch" or "bookmark") +- `title`: String +- `slug`: String +- `published_at`: DateTime +- `excerpt`: String +- `tags`: String +- `content`: Text (required for dispatch) +- `url`: String (required for bookmark) + +### Page +- `id`: Integer +- `title`: String +- `slug`: String +- `content`: Text + diff --git a/Gemfile b/Gemfile index 24385f8..53921c9 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,8 @@ gem "httparty" gem "dotenv" # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] # gem "kredis" - +#api stuff +gem "jsonapi-serializer" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 8b2c738..a891a36 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,6 +122,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -318,6 +320,7 @@ DEPENDENCIES devise dotenv httparty + jsonapi-serializer kaminari mysql2 (~> 0.5) puma (>= 5.0) diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..87beaac --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,28 @@ +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2024 [Your Name or Organization] + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +## Additional Terms + +For full terms and conditions, see the complete GNU General Public License at: +https://www.gnu.org/licenses/gpl-3.0.en.html + +## Disclaimer + +This is a short and simplified version of the GNU GPL. It does not constitute legal advice. For legal purposes, please refer to the full text of the GNU GPL v3. + diff --git a/README.md b/README.md index 7db80e4..b07b3af 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,71 @@ -# README +# Arelpe - Ruby on Rails Blog API -This README would normally document whatever steps are necessary to get the -application up and running. +Arelpe is a Ruby on Rails application that provides a RESTful API for managing blog posts and pages. It supports two types of posts: dispatches (regular blog posts) and bookmarks. -Things you may want to cover: +## Features -* Ruby version +- RESTful API for managing posts and pages +- Authentication using API keys +- Pagination and filtering for posts +- Support for two post types: dispatches and bookmarks +- Markdown rendering for post content -* System dependencies +## Requirements -* Configuration +- Ruby 3.3.4 +- Rails 7.2.1 +- MySQL database -* Database creation +## Installation -* Database initialization +1. Clone the repository: + ``` + git clone https://github.com/aidancornelius/arelpe.git + cd arelpe + ``` -* How to run the test suite +2. Install dependencies: + ``` + bundle install + ``` -* Services (job queues, cache servers, search engines, etc.) +3. Set up the database: + ``` + rails db:create + rails db:migrate + ``` -* Deployment instructions +4. Start the Rails server: + ``` + rails server + ``` -* ... +## API Usage + +Please refer to the [API Documentation](API_DOCUMENTATION.md) for detailed information on how to use the API. + +## Testing + +To run the test suite: + +``` +rails test +``` + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## License + +This project is licensed under the GNU GPL - see the [LICENSE.md](LICENSE.md) file for details. + +## Acknowledgments + +- [Devise](https://github.com/heartcombo/devise) for authentication +- [Kaminari](https://github.com/kaminari/kaminari) for pagination +- [jsonapi-serializer](https://github.com/jsonapi-serializer/jsonapi-serializer) for JSON:API serialization \ No newline at end of file diff --git a/app/.DS_Store b/app/.DS_Store index 79b88bf..ba007db 100644 Binary files a/app/.DS_Store and b/app/.DS_Store differ diff --git a/app/controllers/api/.DS_Store b/app/controllers/api/.DS_Store index 523dfc8..4048af6 100644 Binary files a/app/controllers/api/.DS_Store and b/app/controllers/api/.DS_Store differ diff --git a/app/controllers/api/v2/api_controller.rb b/app/controllers/api/v2/api_controller.rb new file mode 100644 index 0000000..f697ac1 --- /dev/null +++ b/app/controllers/api/v2/api_controller.rb @@ -0,0 +1,18 @@ +# app/controllers/api/v2/api_controller.rb +module Api + module V2 + class ApiController < ApplicationController + skip_before_action :verify_authenticity_token + before_action :authenticate_api_key + + private + + def authenticate_api_key + api_key = request.headers['X-API-Key'] || params[:api_key] + unless ApiKey.exists?(key: api_key) + render json: { errors: [{ status: '401', title: 'Invalid API key' }] }, status: :unauthorized + end + end + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v2/pages_controller.rb b/app/controllers/api/v2/pages_controller.rb new file mode 100644 index 0000000..d72d05d --- /dev/null +++ b/app/controllers/api/v2/pages_controller.rb @@ -0,0 +1,23 @@ +# app/controllers/api/v2/pages_controller.rb +module Api + module V2 + class PagesController < ApiController + # GET /api/v2/pages + def index + pages = Page.where(visibility: :visible) + render json: PageSerializer.new(pages).serializable_hash + end + + # GET /api/v2/pages/:id + def show + page = Page.find_by(slug: params[:id], visibility: :visible) + + if page + render json: PageSerializer.new(page).serializable_hash + else + render json: { errors: [{ status: '404', title: 'Page not found' }] }, status: :not_found + end + end + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v2/posts_controller.rb b/app/controllers/api/v2/posts_controller.rb new file mode 100644 index 0000000..12ea991 --- /dev/null +++ b/app/controllers/api/v2/posts_controller.rb @@ -0,0 +1,77 @@ +# app/controllers/api/v2/posts_controller.rb +module Api + module V2 + class PostsController < ApiController + # GET /api/v2/posts + def index + page = params[:page].presence || 1 + per_page = params[:per_page].presence || 15 + filter = params[:filter] || 'all' + + page = page.to_i + per_page = per_page.to_i + + # Ensure per_page is not zero or negative + per_page = 15 if per_page <= 0 + + posts = Post.get_posts_and_bookmarks_with_pagination(page, per_page, filter) + + render json: PostSerializer.new(posts, meta: pagination_meta(posts)).serializable_hash + end + + # GET /api/v2/posts/:id + def show + post = Post.find(params[:id]) + render json: PostSerializer.new(post).serializable_hash + end + + # POST /api/v2/posts + def create + post = Post.new(post_params) + + if post.save + render json: PostSerializer.new(post).serializable_hash, status: :created + else + render json: error_response(post.errors), status: :unprocessable_entity + end + end + + # PATCH/PUT /api/v2/posts/:id + def update + post = Post.find(params[:id]) + + if post.update(post_params) + render json: PostSerializer.new(post).serializable_hash + else + render json: error_response(post.errors), status: :unprocessable_entity + end + end + + # DELETE /api/v2/posts/:id + def destroy + post = Post.find(params[:id]) + post.destroy + head :no_content + end + + private + + def post_params + params.require(:data).require(:attributes).permit(:post_type, :title, :content, :tags, :url, :published_at) + end + + def pagination_meta(posts) + { + current_page: posts.current_page, + total_pages: posts.total_pages, + total_count: posts.total_count, + per_page: posts.limit_value + } + end + + def error_response(errors) + { errors: errors.full_messages.map { |message| { status: '422', title: message } } } + end + end + end +end \ No newline at end of file diff --git a/app/models/post.rb b/app/models/post.rb index d39b61c..8c63ab2 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -15,14 +15,16 @@ class Post < ApplicationRecord scope :bookmarks, -> { where(post_type: 'bookmark') } def self.get_posts_and_bookmarks_with_pagination(page, per_page, filter) - case filter - when 'posts' - dispatches - when 'bookmarks' - bookmarks - else - all - end.order(published_at: :desc).page(page).per(per_page) + posts = case filter + when 'posts' + dispatches + when 'bookmarks' + bookmarks + else + all + end + + posts.order(published_at: :desc).page(page).per(per_page) end def rss_time @@ -42,15 +44,20 @@ class Post < ApplicationRecord end def format_tags + return "" if tags.blank? + tags.split(/\s*(?:,|\s+and)\s*/).map { |tag| "#{tag.strip}" }.join(', ') + '.' end def generate_excerpt(max_length = 180) + return "" if content.blank? + stripped_content = ActionController::Base.helpers.strip_tags(content) - excerpt = stripped_content.split('.').first if stripped_content.present? || stripped_content[0...max_length] if stripped_content.present? - excerpt.gsub!("Dear friends,", "") if excerpt.present? - excerpt.gsub!(/\s+/, ' ') if excerpt.present? - excerpt.strip if excerpt.present? + excerpt = stripped_content.split('.').first || stripped_content[0...max_length] + excerpt = excerpt[0...max_length] if excerpt.length > max_length + excerpt.gsub!("Dear friends,", "") + excerpt.gsub!(/\s+/, ' ') + excerpt.strip end private diff --git a/app/serializers/page_serializer.rb b/app/serializers/page_serializer.rb new file mode 100644 index 0000000..cf82558 --- /dev/null +++ b/app/serializers/page_serializer.rb @@ -0,0 +1,5 @@ +class PageSerializer + include JSONAPI::Serializer + + attributes :title, :slug, :content +end \ No newline at end of file diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb new file mode 100644 index 0000000..b8356ae --- /dev/null +++ b/app/serializers/post_serializer.rb @@ -0,0 +1,21 @@ +class PostSerializer + include JSONAPI::Serializer + + attributes :post_type, :title, :slug, :published_at, :tags, :url, :content + + attribute :excerpt do |object| + object.excerpt.presence || object.generate_excerpt + end + + attribute :formatted_date do |object| + object.published_at&.strftime("%B %d, %Y") + end + + attribute :reading_time do |object| + if object.content.present? + (object.content.split.size / 200.0).ceil + else + nil + end + end +end diff --git a/config/routes.rb b/config/routes.rb index aa059e6..9e62840 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,10 @@ Rails.application.routes.draw do match 'api', to: 'api#handle_request', via: [:get, :post] get 'status', to: 'api#status' end + namespace :v2 do + resources :posts, only: [:index, :show, :create, :update, :destroy] + resources :pages, only: [:index, :show] + end end resources :pages