--- /dev/null
+# 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
+
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"
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)
devise
dotenv
httparty
+ jsonapi-serializer
kaminari
mysql2 (~> 0.5)
puma (>= 5.0)
--- /dev/null
+# 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.
+
-# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
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
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
--- /dev/null
+class PageSerializer
+ include JSONAPI::Serializer
+
+ attributes :title, :slug, :content
+end
\ No newline at end of file
--- /dev/null
+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
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