From fb78bcb0b0a01ca079f0fdafb4631b1bf6667da8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:36:22 +0000 Subject: [PATCH 01/10] Add aliases --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index cbdfbbf89..c879d4f22 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -30,4 +30,12 @@ class Activity < ApplicationRecord def to_s name end + + def garden_name + garden&.name + end + + def planting_name + planting&.name + end end From bb4e2dd788bd765e15a79e5a3a02493181a29965 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 22:07:24 +0930 Subject: [PATCH 02/10] Add aliases (#4232) --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index cbdfbbf89..c879d4f22 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -30,4 +30,12 @@ class Activity < ApplicationRecord def to_s name end + + def garden_name + garden&.name + end + + def planting_name + planting&.name + end end From 2f0b8e9d765eea34982025d64fec5712066e48e9 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:40:06 +0000 Subject: [PATCH 03/10] Add aliases --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index c879d4f22..b16c3deaf 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -35,7 +35,15 @@ class Activity < ApplicationRecord garden&.name end + def garden_slug + garden&.slug + end + def planting_name planting&.name end + + def planting_slug + planting&.slug + end end From b2e959aded70814af43283d10735aa72dd6c2f82 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:44:40 +0000 Subject: [PATCH 04/10] Delegate --- app/models/activity.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/activity.rb b/app/models/activity.rb index b16c3deaf..30bde60e1 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -40,10 +40,10 @@ class Activity < ApplicationRecord end def planting_name - planting&.name + planting&.crop&.name end def planting_slug - planting&.slug + planting&.crop&.slug end end From e322871740ad66e86443ea0e6bcde91a93b834bf Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:49:17 +0000 Subject: [PATCH 05/10] Fix UX --- app/views/plantings/show.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 21a6d0635..dc5a10f55 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -89,11 +89,11 @@ - else .col-md-12 %p Nothing is currently planned here. - - if @finished_activities&.size&.positive? - %h2 Finished activities for planting - .index-cards - - @finished_activities.each do |activity| - = render "activities/card", activity: activity + - if @finished_activities&.size&.positive? + %h2 Finished activities for planting + .index-cards + - @finished_activities.each do |activity| + = render "activities/card", activity: activity .col-md-4.col-xs-12 = render @planting.crop From cf8380029ac2654551295a333604065f0f3f49f4 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 10:19:08 +0000 Subject: [PATCH 06/10] Rubocop --- lib/tasks/wikidata.rake | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/tasks/wikidata.rake b/lib/tasks/wikidata.rake index e1f117a67..8ce31b942 100644 --- a/lib/tasks/wikidata.rake +++ b/lib/tasks/wikidata.rake @@ -36,21 +36,21 @@ namespace :wikidata do aliases = wikidata_data['entities'][wikidata_id]['aliases'] aliases.each do |lang, values| values.each do |value| - unless AlternateName.exists?(name: value['value'], language: lang, crop: crop) - AlternateName.create!( - name: value['value'], - language: lang, - crop: crop, - creator: creator - ) - puts " Added alternate name: #{value['value']} (#{lang})" - end + next if AlternateName.exists?(name: value['value'], language: lang, crop: crop) + + AlternateName.create!( + name: value['value'], + language: lang, + crop: crop, + creator: creator + ) + puts " Added alternate name: #{value['value']} (#{lang})" end end else puts " Could not find Wikidata ID for #{crop.name}" end - rescue => e + rescue StandardError => e puts " Error processing crop #{crop.name}: #{e.message}" end end From 02db5b81306d5722f02136319f66c0ee51df5e3e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:50:06 +0930 Subject: [PATCH 07/10] Add API token generation, authentication, and CRUD for a number of the API resources (#4237) * feat: Add API token generation and authentication This commit introduces API token generation and authentication for write operations. - Adds a section to the user's profile edit page to generate and display an API token. - Reuses the `authentications` table to store the API token, avoiding the need for a database migration. - Implements token-based authentication for the API using the `Authorization: Token token=...` header. - Enables write operations for all API resources and ensures they are protected by the new authentication mechanism. - Adds feature and request specs to test the new functionality. * feat: Add API token generation and authentication This commit introduces API token generation and authentication for write operations. - Adds a section to the user's profile edit page to generate and display an API token. - Reuses the `authentications` table to store the API token, avoiding the need for a database migration. - Implements token-based authentication for the API using the `Authorization: Token token=...` header. - Enables write operations for all API resources and ensures they are protected by the new authentication mechanism. - Adds feature and request specs to test the new functionality. * Mark as editable * Refactor * WIP - Authentication * Implement more test coverage * Split 401 and 403 * Before Create hooks * Update harvest specs, defaulting to the first plant part - this may not be right * Update coverage * Update coverage * Rubocop * Rubocop * Rubocop * Fix coverage * For now, mark photos immutable again * Fix specs * Fix specs * Rubocop * Fix specs --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Daniel O'Connor --- .rubocop_todo.yml | 2 +- .../api/v1/activities_controller.rb | 2 - app/controllers/api/v1/base_controller.rb | 34 ++++ app/controllers/registrations_controller.rb | 6 + app/models/member.rb | 14 ++ app/resources/api/v1/activity_resource.rb | 4 +- app/resources/api/v1/crop_resource.rb | 3 +- app/resources/api/v1/garden_resource.rb | 4 +- app/resources/api/v1/harvest_resource.rb | 8 +- app/resources/api/v1/photo_resource.rb | 5 +- app/resources/api/v1/planting_resource.rb | 4 +- app/resources/api/v1/seed_resource.rb | 4 +- app/resources/base_resource.rb | 12 +- .../devise/registrations/_edit_apps.html.haml | 13 ++ config/initializers/jsonapi_resources.rb | 4 +- config/locales/devise.en.yml | 1 + config/routes.rb | 1 + .../features/members/token_management_spec.rb | 38 +++++ .../api/v1/activities_request_spec.rb | 28 ++-- spec/requests/api/v1/gardens_request_spec.rb | 122 +++++++++++++-- spec/requests/api/v1/harvests_request_spec.rb | 145 ++++++++++++++--- .../requests/api/v1/plantings_request_spec.rb | 146 +++++++++++++++--- spec/requests/api/v1/seeds_request_spec.rb | 137 +++++++++++++--- 23 files changed, 624 insertions(+), 113 deletions(-) create mode 100644 spec/features/members/token_management_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3195873c1..fbba7cede 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -314,7 +314,7 @@ RSpec/MultipleExpectations: # Offense count: 138 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: - Max: 14 + Max: 20 # Offense count: 133 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. diff --git a/app/controllers/api/v1/activities_controller.rb b/app/controllers/api/v1/activities_controller.rb index a79f9c778..eaeb4c085 100644 --- a/app/controllers/api/v1/activities_controller.rb +++ b/app/controllers/api/v1/activities_controller.rb @@ -2,8 +2,6 @@ module Api module V1 - # This controller is intentionally empty. - # The `jsonapi-resources` gem provides the necessary actions. class ActivitiesController < BaseController end end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 84eb42dcc..bf69de691 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -4,6 +4,40 @@ module Api module V1 class BaseController < JSONAPI::ResourceController abstract + protect_from_forgery with: :null_session + before_action :authenticate_member_from_token! + before_action :enforce_member_for_write_operations!, only: %i(create update destroy) + rescue_from CanCan::AccessDenied do + head :forbidden + end + + def context + { + current_user: current_user, + current_ability: current_ability, + controller: self, + action: params[:action] + } + end + + private + + attr_reader :current_user + + def enforce_member_for_write_operations! + head :unauthorized unless current_user + end + + def authenticate_member_from_token! + authenticate_with_http_token do |token, _options| + auth = Authentication.find_by(token: token, provider: 'api') + if auth.present? + @current_user = auth.member + + return true + end + end + end end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 2bb8b1764..90593eaca 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -38,6 +38,12 @@ class RegistrationsController < Devise::RegistrationsController end end + def regenerate_api_token + current_member.regenerate_api_token + set_flash_message :notice, :api_token_regenerated + redirect_to edit_member_registration_path + '#apps' + end + def destroy if @member.valid_password?(params.require(:member)[:current_password]) @member.discard diff --git a/app/models/member.rb b/app/models/member.rb index 2427092f6..f9b1c6942 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -24,6 +24,20 @@ class Member < ApplicationRecord has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification" has_many :authentications, dependent: :destroy + has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy + + def api_token? + api_token.present? + end + + def regenerate_api_token + api_token.destroy if api_token? + create_api_token( + provider: 'api', + uid: id, + token: SecureRandom.hex(16) + ) + end has_many :photos, inverse_of: :owner has_many :likes, dependent: :destroy diff --git a/app/resources/api/v1/activity_resource.rb b/app/resources/api/v1/activity_resource.rb index 72256146b..5803a7686 100644 --- a/app/resources/api/v1/activity_resource.rb +++ b/app/resources/api/v1/activity_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class ActivityResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :garden diff --git a/app/resources/api/v1/crop_resource.rb b/app/resources/api/v1/crop_resource.rb index 789884392..ed4cac67c 100644 --- a/app/resources/api/v1/crop_resource.rb +++ b/app/resources/api/v1/crop_resource.rb @@ -3,8 +3,7 @@ module Api module V1 class CropResource < BaseResource - immutable - + immutable # TODO: Re-evaluate this later filter :approval_status, default: 'approved' has_many :plantings diff --git a/app/resources/api/v1/garden_resource.rb b/app/resources/api/v1/garden_resource.rb index 47dcd7858..cc94847a3 100644 --- a/app/resources/api/v1/garden_resource.rb +++ b/app/resources/api/v1/garden_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class GardenResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/harvest_resource.rb b/app/resources/api/v1/harvest_resource.rb index 2013390a6..8f086dc55 100644 --- a/app/resources/api/v1/harvest_resource.rb +++ b/app/resources/api/v1/harvest_resource.rb @@ -3,11 +3,17 @@ module Api module V1 class HarvestResource < BaseResource - immutable + before_save do + @model.owner = context[:current_user] + @model.crop_id = @model.planting.crop_id if @model.planting_id + @model.harvested_at = Time.zone.now if @model.harvested_at.blank? + @model.plant_part = PlantPart.first + end has_one :crop has_one :planting has_one :owner, class_name: 'Member' + # has_one :plant_part has_many :photos attribute :harvested_at diff --git a/app/resources/api/v1/photo_resource.rb b/app/resources/api/v1/photo_resource.rb index 6da294cd8..4f6c08223 100644 --- a/app/resources/api/v1/photo_resource.rb +++ b/app/resources/api/v1/photo_resource.rb @@ -3,7 +3,10 @@ module Api module V1 class PhotoResource < BaseResource - immutable + immutable # TODO: Re-evaluate this. + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/planting_resource.rb b/app/resources/api/v1/planting_resource.rb index 8a5bd4659..3ab2c4fc8 100644 --- a/app/resources/api/v1/planting_resource.rb +++ b/app/resources/api/v1/planting_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class PlantingResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :garden has_one :crop diff --git a/app/resources/api/v1/seed_resource.rb b/app/resources/api/v1/seed_resource.rb index dc1016cd9..9c69e493a 100644 --- a/app/resources/api/v1/seed_resource.rb +++ b/app/resources/api/v1/seed_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class SeedResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :crop diff --git a/app/resources/base_resource.rb b/app/resources/base_resource.rb index 46de0ce53..b85ba45e1 100644 --- a/app/resources/base_resource.rb +++ b/app/resources/base_resource.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true class BaseResource < JSONAPI::Resource - immutable abstract + + [:create, :update, :remove].each do |action| + set_callback action, :before, :authorize + end + + # Check authorisation for write operations. + # NOTE: At a later time, we may require API tokens for READ operations. + def authorize + # context[:action] is simply context[:controller].params[:action] + context[:current_ability].authorize! context[:action].to_sym, @model + end end diff --git a/app/views/devise/registrations/_edit_apps.html.haml b/app/views/devise/registrations/_edit_apps.html.haml index 12e267bb0..24fb472e4 100644 --- a/app/views/devise/registrations/_edit_apps.html.haml +++ b/app/views/devise/registrations/_edit_apps.html.haml @@ -15,3 +15,16 @@ method: :delete, class: "remove btn btn-danger" - else = link_to 'Connect to Flickr', '/members/auth/flickr', class: 'btn' + %hr + .row + .col-md-12 + %p + = image_tag "icons/post.svg", size: "32x32", alt: 'API logo' + - if current_member.api_token? + Your API token is + %code= current_member.api_token.token + = link_to "Regenerate", regenerate_api_token_path, + data: { confirm: "Are you sure? Your old token will stop working immediately." }, + method: :post, class: "remove btn btn-danger" + - else + = link_to 'Generate API Token', regenerate_api_token_path, method: :post, class: 'btn btn-primary' diff --git a/config/initializers/jsonapi_resources.rb b/config/initializers/jsonapi_resources.rb index 8cf1a906b..7f0c57c29 100644 --- a/config/initializers/jsonapi_resources.rb +++ b/config/initializers/jsonapi_resources.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true - +class UnauthorisedError < JSONAPI::Error +end JSONAPI.configure do |config| # built in paginators are :none, :offset, :paged config.default_paginator = :offset config.default_page_size = 10 config.maximum_page_size = 100 + config.exception_class_whitelist = [CanCan::AccessDenied, UnauthorisedError] end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 3fa1481e0..ffd459cf5 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -54,6 +54,7 @@ en: You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address. destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + api_token_regenerated: 'Your API token has been regenerated.' unlocks: send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' unlocked: 'Your account has been unlocked successfully. Please sign in to continue.' diff --git a/config/routes.rb b/config/routes.rb index 700acc657..3b68bc8f1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,7 @@ Rails.application.routes.draw do } devise_scope :member do get '/members/unsubscribe/:message' => 'members#unsubscribe', as: 'unsubscribe_member' + post '/members/regenerate_api_token' => 'registrations#regenerate_api_token', as: 'regenerate_api_token' end match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup diff --git a/spec/features/members/token_management_spec.rb b/spec/features/members/token_management_spec.rb new file mode 100644 index 000000000..6fcc77366 --- /dev/null +++ b/spec/features/members/token_management_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "member token management", :js do + include_context 'signed in member' + + before do + visit edit_member_registration_path + click_on "Apps" + end + + it "can generate an API token" do + expect(page).to have_no_content("Your API token is") + click_on "Generate API Token" + expect(page).to have_content("Your API token is") + member.reload + expect(member.api_token).to be_present + end + + context "with an existing token" do + before do + member.regenerate_api_token + visit edit_member_registration_path + click_on "Apps" + end + + it "can regenerate an API token" do + old_token = member.api_token.token + expect(page).to have_content("Your API token is") + accept_confirm do + click_on "Regenerate" + end + expect(page).to have_content("Your API token is") + expect(member.reload.api_token.token).not_to eq(old_token) + end + end +end diff --git a/spec/requests/api/v1/activities_request_spec.rb b/spec/requests/api/v1/activities_request_spec.rb index 553c39856..0eb5e98a8 100644 --- a/spec/requests/api/v1/activities_request_spec.rb +++ b/spec/requests/api/v1/activities_request_spec.rb @@ -23,34 +23,34 @@ RSpec.describe 'Activities', type: :request do it 'filters by owner' do get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by garden' do - get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) + get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by planting' do - get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) + get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by category' do - get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) + get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) - expect(subject['data'][1]['id']).to eq(activity2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(subject['data'][1]['id']).to eq(activity2.id.to_s) end end end diff --git a/spec/requests/api/v1/gardens_request_spec.rb b/spec/requests/api/v1/gardens_request_spec.rb index 344ea74d1..de3906db8 100644 --- a/spec/requests/api/v1/gardens_request_spec.rb +++ b/spec/requests/api/v1/gardens_request_spec.rb @@ -52,18 +52,19 @@ RSpec.describe 'Gardens', type: :request do context 'filtering' do let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) } + pending 'filters by active' do get('/api/v1/gardens?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden.id.to_s) end it 'filters by garden_type' do get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:) - - expect(response.status).to eq 200 + + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden2.id.to_s) end @@ -71,27 +72,116 @@ RSpec.describe 'Gardens', type: :request do it 'filters by owner' do get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(2) expect(subject['data'][1]['id']).to eq(garden2.id.to_s) end end - it '#create' do - expect do - post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden_params) do + { + data: { + type: 'gardens', + attributes: { + name: 'My API Garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/gardens', params: garden_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/gardens', params: garden_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.gardens.count).to eq(2) # 1 from after_create callback, 1 from api + end end - it '#update' do - expect do - post "/api/v1/gardens/#{garden.id}", params: { 'garden' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + let(:update_params) do + { + data: { + type: 'gardens', + id: garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own garden' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(garden.reload.name).to eq('An updated garden') + end + + it 'returns 403 Forbidden for another member\'s garden' do + update_params_for_other = { + data: { + type: 'gardens', + id: other_member_garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/gardens/#{garden.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/gardens/#{garden.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own garden' do + delete "/api/v1/gardens/#{garden.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: garden.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s garden' do + delete "/api/v1/gardens/#{other_member_garden.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/harvests_request_spec.rb b/spec/requests/api/v1/harvests_request_spec.rb index f49b7b88f..38d6777c1 100644 --- a/spec/requests/api/v1/harvests_request_spec.rb +++ b/spec/requests/api/v1/harvests_request_spec.rb @@ -78,6 +78,7 @@ RSpec.describe 'Harvests', type: :request do context 'filtering' do let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) } + it 'filters by crop' do get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -87,47 +88,141 @@ RSpec.describe 'Harvests', type: :request do it 'filters by planting' do get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by plant_part' do - get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) + get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by owner' do - get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) + get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end end - it '#create' do - expect do - put '/api/v1/harvests', headers:, params: { - 'harvest' => { 'description' => 'can i make this' } - } - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:planting) { create(:planting, owner: member) } + let(:plant_part) { create(:plant_part) } + let(:harvest_params) do + { + data: { + type: 'harvests', + attributes: { + description: 'My API harvests' + }, + relationships: { + planting: { data: { type: 'plantings', id: planting.id } } + # plant_part: { data: { type: 'plant_parts', id: plant_part.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/harvests', params: harvest_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/harvests', params: harvest_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.harvests.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/harvests/#{harvest.id}", headers:, params: { - 'harvest' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + let(:update_params) do + { + data: { + type: 'harvests', + id: harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own harvest' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(harvest.reload.description).to eq('An updated harvest') + end + + it 'returns 403 Forbidden for another member\'s harvest' do + update_params_for_other = { + data: { + type: 'harvests', + id: other_member_harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/harvests/#{harvest.id}", headers:, params: {} - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/harvests/#{harvest.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own harvest' do + delete "/api/v1/harvests/#{harvest.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: harvest.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s harvest' do + delete "/api/v1/harvests/#{other_member_harvest.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/plantings_request_spec.rb b/spec/requests/api/v1/plantings_request_spec.rb index 5a406afe9..7d334d539 100644 --- a/spec/requests/api/v1/plantings_request_spec.rb +++ b/spec/requests/api/v1/plantings_request_spec.rb @@ -95,24 +95,119 @@ RSpec.describe 'Plantings', type: :request do expect(subject['data']).to eq(planting_encoded_as_json_api) end - it '#create' do - expect do - post '/api/v1/plantings', params: { 'planting' => { 'description' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:garden) { create(:garden, owner: member) } + let(:planting_params) do + { + data: { + type: 'plantings', + attributes: { + description: 'My API plantings' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } }, + garden: { data: { type: 'gardens', id: garden.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/plantings', params: planting_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/plantings', params: planting_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.plantings.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/plantings/#{planting.id}", headers:, params: { - 'planting' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + let(:update_params) do + { + data: { + type: 'plantings', + id: planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own planting' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(planting.reload.description).to eq('An updated planting') + end + + it 'returns 403 Forbidden for another member\'s planting' do + update_params_for_other = { + data: { + type: 'plantings', + id: other_member_planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/plantings/#{planting.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/plantings/#{planting.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own planting' do + delete "/api/v1/plantings/#{planting.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: planting.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s planting' do + delete "/api/v1/plantings/#{other_member_planting.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end describe "by member/owner" do @@ -144,6 +239,7 @@ RSpec.describe 'Plantings', type: :request do context 'filtering' do let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') } let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) } + it 'filters by failed' do get('/api/v1/plantings?filter[failed]=true', params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -151,25 +247,25 @@ RSpec.describe 'Plantings', type: :request do end it 'filters by sunniness' do - get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(planting2.id.to_s) + get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(planting2.id.to_s) end it 'filters by perennial' do - get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) + get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) end it 'filters by active' do - get('/api/v1/plantings?filter[active]=true', params: {}, headers:) + get('/api/v1/plantings?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(planting.id.to_s) end end end diff --git a/spec/requests/api/v1/seeds_request_spec.rb b/spec/requests/api/v1/seeds_request_spec.rb index 00ee04684..ea30c7924 100644 --- a/spec/requests/api/v1/seeds_request_spec.rb +++ b/spec/requests/api/v1/seeds_request_spec.rb @@ -61,39 +61,136 @@ RSpec.describe 'Seeds', type: :request do it { expect(subject['data']).to eq(seed_encoded_as_json_api) } end - it '#create' do - expect do - post '/api/v1/seeds', params: { 'seed' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed_params) do + { + data: { + type: 'seeds', + attributes: { + description: 'My API seeds' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/seeds', params: seed_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/seeds', params: seed_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.seeds.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/seeds/#{seed.id}", params: { 'seed' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + let(:update_params) do + { + data: { + type: 'seeds', + id: seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own seed' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(seed.reload.description).to eq('An updated seed') + end + + it 'returns 403 Forbidden for another member\'s seed' do + update_params_for_other = { + data: { + type: 'seeds', + id: other_member_seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/seeds/#{seed.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let!(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/seeds/#{seed.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own seed' do + delete "/api/v1/seeds/#{seed.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Seed.find_by(id: seed.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s seed' do + delete "/api/v1/seeds/#{other_member_seed.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end context 'filtering' do - let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') } + let!(:seed2) do + FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') + end + it 'filters by crop' do get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end - it 'filters by tradable_to' do get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -101,7 +198,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by organic' do get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -109,7 +206,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by gmo' do get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -117,7 +214,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by heirloom' do get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -125,7 +222,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by owner' do get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end From 7988080054e3504cd0d938fb1a433e7bb0461464 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 19:52:44 +0930 Subject: [PATCH 08/10] Update .rubocop.yml --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index c2370187b..a6b7784fa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ inherit_from: .rubocop_todo.yml -require: +plugins: - rubocop-factory_bot - rubocop-capybara - rubocop-rails From e5bf9d98e6905b5699f427bb66d97cc32229469b Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 19:56:12 +0930 Subject: [PATCH 09/10] Rubocop (#4241) --- app/controllers/activities_controller.rb | 2 +- app/controllers/registrations_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 08e8aa4bd..8ca7f956e 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -29,7 +29,7 @@ class ActivitiesController < DataController def new @activity = Activity.new( - owner: current_member, + owner: current_member, due_date: Date.today ) if params[:garden_id] diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 90593eaca..88c569c83 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha. def edit - @flickr_auth = current_member.auth('flickr') + @flickr_auth = current_member.auth('flickr') render "edit" end From a76ef6a11756c8eae2c2bd6f008719255de5462e Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 13:56:29 +0000 Subject: [PATCH 10/10] Format --- swagger/v1/swagger.json | 4748 +++++++++++++++++++-------------------- 1 file changed, 2374 insertions(+), 2374 deletions(-) diff --git a/swagger/v1/swagger.json b/swagger/v1/swagger.json index 9ac938a56..64531dc66 100644 --- a/swagger/v1/swagger.json +++ b/swagger/v1/swagger.json @@ -1,114 +1,388 @@ { "swagger": "2.0", "info": { - "title": "API V1", - "version": "V1" -}, - "basePath" : "/api/v1", + "title": "API V1", + "version": "V1" + }, + "basePath": "/api/v1", "paths": { - "/crops": { - "get": { - "summary": "crops List", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[approval_status]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false + "/crops": { + "get": { + "summary": "crops List", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[approval_status]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, + "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + }, + "en_wikipedia_url": { + "type": "string", + "x-nullable": true, + "description": "Wikipedia URL (English)" + }, + "perennial": { + "type": "boolean", + "x-nullable": true, + "description": "Is the item perennial? (A plant that lives more than two years)" + }, + "median_lifespan": { + "type": "integer", + "x-nullable": true, + "description": "Median lifespan" + }, + "median_days_to_first_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to first harvest" + }, + "median_days_to_last_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to last harvest" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "parent": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" + } + } + }, + "description": "Data" + }, + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } + } } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + } + }, + "/crops/{id}": { + "get": { + "summary": "crops Detail", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -261,381 +535,309 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/crops/{id}": { - "get": { - "summary": "crops Detail", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/gardens": { + "get": { + "summary": "gardens List", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - }, - "en_wikipedia_url": { - "type": "string", - "x-nullable": true, - "description": "Wikipedia URL (English)" - }, - "perennial": { - "type": "boolean", - "x-nullable": true, - "description": "Is the item perennial? (A plant that lives more than two years)" - }, - "median_lifespan": { - "type": "integer", - "x-nullable": true, - "description": "Median lifespan" - }, - "median_days_to_first_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to first harvest" - }, - "median_days_to_last_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to last harvest" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "parent": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/gardens": { - "get": { - "summary": "gardens List", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/gardens/{id}": { + "get": { + "summary": "gardens Detail", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -723,337 +925,396 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/gardens/{id}": { - "get": { - "summary": "gardens Detail", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/members": { + "get": { + "summary": "members List", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,login_name,slug", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[login_name]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" }, - "plantings": { + "attributes": { "type": "object", "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" + "login_name": { + "type": "string", + "x-nullable": true, + "description": "Login name" + }, + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" } }, - "description": "Related model" + "description": "Attributes" }, - "photos": { + "relationships": { "type": "object", "properties": { - "links": { + "gardens": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/members": { - "get": { - "summary": "members List", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,login_name,slug", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[login_name]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/members/{id}": { + "get": { + "summary": "members Detail", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1186,375 +1447,368 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/members/{id}": { - "get": { - "summary": "members Detail", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/harvests": { + "get": { + "summary": "harvests List", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "harvested_at": { + "type": "string", + "x-nullable": true, + "description": "Harvested date time" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "unit": { + "type": "string", + "x-nullable": true, + "description": "Unit" + }, + "weight_quantity": { + "type": "string", + "x-nullable": true, + "description": "Weight/Quanitity" + }, + "weight_unit": { + "type": "string", + "x-nullable": true, + "description": "Weight Unit" + }, + "si_weight": { + "type": "string", + "x-nullable": true, + "description": "SI Weight" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "planting": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "login_name": { - "type": "string", - "x-nullable": true, - "description": "Login name" - }, - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/harvests": { - "get": { - "summary": "harvests List", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/harvests/{id}": { + "get": { + "summary": "harvests Detail", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1571,7 +1825,7 @@ "harvested_at": { "type": "string", "x-nullable": true, - "description": "Harvested date time" + "description": "Harvested datetime" }, "description": { "type": "string", @@ -1586,7 +1840,7 @@ "weight_quantity": { "type": "string", "x-nullable": true, - "description": "Weight/Quanitity" + "description": "Weight/Quantity" }, "weight_unit": { "type": "string", @@ -1687,354 +1941,315 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/harvests/{id}": { - "get": { - "summary": "harvests Detail", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/seeds": { + "get": { + "summary": "seeds List", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanitity" + }, + "plant_before": { + "type": "string", + "x-nullable": true, + "description": "Plant before" + }, + "tradable_to": { + "type": "string", + "x-nullable": true, + "description": "Tradeable to" + }, + "days_until_maturity_min": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (min)" + }, + "days_until_maturity_max": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (max)" + }, + "organic": { + "type": "string", + "x-nullable": true, + "description": "Organic" + }, + "gmo": { + "type": "string", + "x-nullable": true, + "description": "GMO" + }, + "heirloom": { + "type": "string", + "x-nullable": true, + "description": "Heirloom" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "harvested_at": { - "type": "string", - "x-nullable": true, - "description": "Harvested datetime" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "unit": { - "type": "string", - "x-nullable": true, - "description": "Unit" - }, - "weight_quantity": { - "type": "string", - "x-nullable": true, - "description": "Weight/Quantity" - }, - "weight_unit": { - "type": "string", - "x-nullable": true, - "description": "Weight Unit" - }, - "si_weight": { - "type": "string", - "x-nullable": true, - "description": "SI Weight" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "planting": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/seeds": { - "get": { - "summary": "seeds List", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/seeds/{id}": { + "get": { + "summary": "seeds Detail", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2056,7 +2271,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanitity" + "description": "Quanity" }, "plant_before": { "type": "string", @@ -2066,7 +2281,7 @@ "tradable_to": { "type": "string", "x-nullable": true, - "description": "Tradeable to" + "description": "Tradable to" }, "days_until_maturity_min": { "type": "integer", @@ -2142,371 +2357,420 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/seeds/{id}": { - "get": { - "summary": "seeds Detail", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/plantings": { + "get": { + "summary": "plantings List", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[crop]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[planted_from]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[garden]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[owner]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[finished]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quanity" - }, - "plant_before": { - "type": "string", - "x-nullable": true, - "description": "Plant before" - }, - "tradable_to": { - "type": "string", - "x-nullable": true, - "description": "Tradable to" - }, - "days_until_maturity_min": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (min)" - }, - "days_until_maturity_max": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (max)" - }, - "organic": { - "type": "string", - "x-nullable": true, - "description": "Organic" - }, - "gmo": { - "type": "string", - "x-nullable": true, - "description": "GMO" - }, - "heirloom": { - "type": "string", - "x-nullable": true, - "description": "Heirloom" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { + "attributes": { "type": "object", "properties": { - "links": { + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" + }, + "planted_at": { + "type": "string", + "x-nullable": true, + "description": "Planted at" + }, + "finished": { + "type": "boolean", + "x-nullable": false, + "description": "Finished?" + }, + "finished_at": { + "type": "string", + "x-nullable": true, + "description": "Finished at" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanity" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "sunniness": { + "type": "string", + "x-nullable": true, + "description": "Sunniness" + }, + "planted_from": { + "type": "string", + "x-nullable": true, + "description": "Planted from" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "garden": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { + "description": "Related model" + }, + "crop": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/plantings": { - "get": { - "summary": "plantings List", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[crop]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[planted_from]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[garden]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[owner]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[finished]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/plantings/{id}": { + "get": { + "summary": "plantings Detail", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2543,7 +2807,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanity" + "description": "Quantity" }, "description": { "type": "string", @@ -2649,378 +2913,363 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/plantings/{id}": { - "get": { - "summary": "plantings Detail", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/photos": { + "get": { + "summary": "photos List", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "thumbnail_url": { + "type": "string", + "x-nullable": false, + "description": "Thumbnail URL" + }, + "fullsize_url": { + "type": "string", + "x-nullable": false, + "description": "Full-size URL" + }, + "license_name": { + "type": "string", + "x-nullable": false, + "description": "License name" + }, + "link_url": { + "type": "string", + "x-nullable": false, + "description": "Link URL" + }, + "title": { + "type": "string", + "x-nullable": false, + "description": "Title" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "gardens": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - }, - "planted_at": { - "type": "string", - "x-nullable": true, - "description": "Planted at" - }, - "finished": { - "type": "boolean", - "x-nullable": false, - "description": "Finished?" - }, - "finished_at": { - "type": "string", - "x-nullable": true, - "description": "Finished at" - }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quantity" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "sunniness": { - "type": "string", - "x-nullable": true, - "description": "Sunniness" - }, - "planted_from": { - "type": "string", - "x-nullable": true, - "description": "Planted from" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "garden": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/photos": { - "get": { - "summary": "photos List", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/photos/{id}": { + "get": { + "summary": "photos Detail", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -3148,266 +3397,17 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "/photos/{id}": { - "get": { - "summary": "photos Detail", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "thumbnail_url": { - "type": "string", - "x-nullable": false, - "description": "Thumbnail URL" - }, - "fullsize_url": { - "type": "string", - "x-nullable": false, - "description": "Full-size URL" - }, - "license_name": { - "type": "string", - "x-nullable": false, - "description": "License name" - }, - "link_url": { - "type": "string", - "x-nullable": false, - "description": "Link URL" - }, - "title": { - "type": "string", - "x-nullable": false, - "description": "Title" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } - }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } } } -} -} +} \ No newline at end of file