]> gitweb.mndrdr.org Git - arelpe.git/commitdiff
v2 API
authorAidan Cornelius-Bell <[email protected]>
Sun, 15 Sep 2024 06:54:27 +0000 (16:24 +0930)
committerAidan Cornelius-Bell <[email protected]>
Sun, 15 Sep 2024 06:54:27 +0000 (16:24 +0930)
14 files changed:
API_DOCUMENTATION.md [new file with mode: 0644]
Gemfile
Gemfile.lock
LICENSE.md [new file with mode: 0644]
README.md
app/.DS_Store
app/controllers/api/.DS_Store
app/controllers/api/v2/api_controller.rb [new file with mode: 0644]
app/controllers/api/v2/pages_controller.rb [new file with mode: 0644]
app/controllers/api/v2/posts_controller.rb [new file with mode: 0644]
app/models/post.rb
app/serializers/page_serializer.rb [new file with mode: 0644]
app/serializers/post_serializer.rb [new file with mode: 0644]
config/routes.rb

diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md
new file mode 100644 (file)
index 0000000..f709764
--- /dev/null
@@ -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 24385f855967e1d4d936cd15f35fa497136690a2..53921c92079e02c5d11b6492d3a7248ecf5c2de8 100644 (file)
--- 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"
 
index 8b2c73854d39a92f8d4291e103efabcc41cd92de..a891a36097ba89da06945992d9a4e35ca9aca87d 100644 (file)
@@ -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 (file)
index 0000000..87beaac
--- /dev/null
@@ -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 <https://www.gnu.org/licenses/>.
+
+## 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.
+
index 7db80e4ca1bf849701dce58a63f09a102cb9f931..b07b3af62e8bad229faf144d95eb10492ec34574 100644 (file)
--- 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
index 79b88bfa87778802d4b5801d881ff82d59a8703e..ba007db5c0d17b3d740be5f188850cfd41f65112 100644 (file)
Binary files a/app/.DS_Store and b/app/.DS_Store differ
index 523dfc8dd37ecd6a875e806055bda34f6ae07f62..4048af6061694b3bf8a217023577c063b7ec5d69 100644 (file)
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 (file)
index 0000000..f697ac1
--- /dev/null
@@ -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 (file)
index 0000000..d72d05d
--- /dev/null
@@ -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 (file)
index 0000000..12ea991
--- /dev/null
@@ -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
index d39b61c0d09702f565979ed160fad1d42ae1c08a..8c63ab2f76f7cf8d9d2fc039e405a4c672c834ef 100644 (file)
@@ -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| "<code>#{tag.strip}</code>" }.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 (file)
index 0000000..cf82558
--- /dev/null
@@ -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 (file)
index 0000000..b8356ae
--- /dev/null
@@ -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
index aa059e60266576fc862c03560cd00fe587d47053..9e62840d6fb2a90e3752ca4a64d1f6be5e9e6a30 100644 (file)
@@ -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