Class: Woods::Unblocked::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/woods/unblocked/client.rb

Overview

REST client for the Unblocked API v1.

Handles document and collection CRUD with rate limiting, retries, and error handling. Uses Net::HTTP for zero external dependencies.

Examples:

client = Client.new(api_token: "ubk_...")
client.put_document(
  collection_id: "uuid",
  title: "Order (model)",
  body: "# Order\n...",
  uri: "https://github.com/org/repo/blob/main/app/models/order.rb"
)

Constant Summary collapse

BASE_URL =
'https://getunblocked.com/api/v1'
MAX_RETRIES =
3
DEFAULT_TIMEOUT =
30
PAGE_SIZE =

Max page size the list endpoint accepts (per API docs).

200
DEFAULT_ICON_URL =

Repo-hosted Woods mark, used as the collection icon when none is given. The live API rejects collection creation without an iconUrl (despite the API docs marking it optional), so a working default matters.

'https://raw.githubusercontent.com/lost-in-the/woods/main/assets/woods-mark-black.svg'

Instance Method Summary collapse

Constructor Details

#initialize(api_token:, rate_limiter: RateLimiter.new) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_token (String)

    Unblocked API token (Personal or Team)

  • rate_limiter (RateLimiter) (defaults to: RateLimiter.new)

    Rate limiter instance

Raises:

  • (ArgumentError)

    if api_token is nil or empty



56
57
58
59
60
61
# File 'lib/woods/unblocked/client.rb', line 56

def initialize(api_token:, rate_limiter: RateLimiter.new)
  raise ArgumentError, 'api_token is required' if api_token.nil? || api_token.to_s.strip.empty?

  @api_token = api_token
  @rate_limiter = rate_limiter
end

Instance Method Details

#all_documents(collection_id:) ⇒ Array<Hash>

List every document in a collection, paging until exhausted.

Filters client-side on ‘collectionId` since the API has no collection filter. ~5 calls for ~1000 documents; each goes through the rate limiter.

Parameters:

  • collection_id (String)

    Collection UUID to filter to

Returns:

  • (Array<Hash>)

    All matching document metadata



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/woods/unblocked/client.rb', line 146

def all_documents(collection_id:)
  docs = []
  after = nil

  loop do
    page = list_documents(limit: PAGE_SIZE, after: after)
    break if page.empty?

    docs.concat(page)
    break if page.size < PAGE_SIZE

    after = page.last['id']
    # A full page with no cursor id would refetch page 1 forever —
    # stop with what we have rather than loop against the budget.
    break if after.nil?
  end

  docs.select { |doc| doc['collectionId'] == collection_id }
end

#create_collection(name:, description:, icon_url: nil) ⇒ Hash

Create a new collection.

Parameters:

  • name (String)

    Collection name (1-32 chars)

  • description (String)

    Collection description (1-4096 chars)

  • icon_url (String, nil) (defaults to: nil)

    Icon URL. The live API rejects creation with a bare 400 when omitted (despite the API docs marking it optional), so nil falls back to DEFAULT_ICON_URL — the repo-hosted Woods mark.

Returns:

  • (Hash)

    { “id” => “collection-uuid”, “name” => “…”, … }



92
93
94
95
96
97
98
# File 'lib/woods/unblocked/client.rb', line 92

def create_collection(name:, description:, icon_url: nil)
  request(:post, 'collections', {
            name: name,
            description: description,
            iconUrl: icon_url || DEFAULT_ICON_URL
          })
end

#delete_document(document_id:) ⇒ Hash

Delete a document by ID.

Parameters:

  • document_id (String)

    Document UUID

Returns:

  • (Hash)

    API response



116
117
118
# File 'lib/woods/unblocked/client.rb', line 116

def delete_document(document_id:)
  request(:delete, "documents/#{document_id}")
end

#list_collectionsArray<Hash>

List all collections.

Returns:

  • (Array<Hash>)

    Collection objects



103
104
105
106
107
108
109
110
# File 'lib/woods/unblocked/client.rb', line 103

def list_collections
  result = request(:get, 'collections')
  # The live API returns a bare JSON array; the envelope fallbacks are
  # defensive (calling ['items'] on an Array raises TypeError).
  return result if result.is_a?(Array)

  result['items'] || result['data'] || [result].flatten.compact
end

#list_documents(limit: PAGE_SIZE, after: nil) ⇒ Array<Hash>

List a single page of documents.

The endpoint returns a bare JSON array of document metadata (no body): ‘id, collectionId, title, uri, createdAt, updatedAt`. Pagination is cursor-based via `after`/`before` (opaque cursors); there is no server-side collection filter.

Parameters:

  • limit (Integer) (defaults to: PAGE_SIZE)

    Page size (1-200)

  • after (String, nil) (defaults to: nil)

    Opaque forward cursor (typically the last id)

Returns:

  • (Array<Hash>)

    One page of document metadata



130
131
132
133
134
135
136
137
# File 'lib/woods/unblocked/client.rb', line 130

def list_documents(limit: PAGE_SIZE, after: nil)
  query = "limit=#{limit}"
  query += "&after=#{URI.encode_www_form_component(after)}" if after
  result = request(:get, "documents?#{query}")
  return result if result.is_a?(Array)

  result['items'] || result['data'] || []
end

#put_document(collection_id:, title:, body:, uri:) ⇒ Hash

Create or update a document (upsert by URI).

Documents are unique by ‘uri` across the organization. If a document with the given URI exists, it is updated; otherwise it is created. Documents become available for queries within ~1 minute.

Parameters:

  • collection_id (String)

    Target collection UUID

  • title (String)

    Document title (plain text)

  • body (String)

    Document body (Markdown preferred)

  • uri (String)

    Source URL (used as unique identifier and citation link)

Returns:

  • (Hash)

    { “id” => “document-uuid” }



74
75
76
77
78
79
80
81
# File 'lib/woods/unblocked/client.rb', line 74

def put_document(collection_id:, title:, body:, uri:)
  request(:put, 'documents', {
            collectionId: collection_id,
            title: title,
            body: body,
            uri: uri
          })
end