Class: Puid::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/puid.rb

Overview

A PUID API client.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil, access_token: nil, endpoint: DEFAULT_ENDPOINT) ⇒ Client

Provide exactly one of api_key: (puid_live_…) or access_token: (puid_at_…).

Raises:



34
35
36
37
38
39
40
# File 'lib/puid.rb', line 34

def initialize(api_key: nil, access_token: nil, endpoint: DEFAULT_ENDPOINT)
  raise Error.new("provide either api_key or access_token, not both") if api_key && access_token
  raise Error.new("provide an api_key (puid_live_…) or an access_token (puid_at_…)") unless api_key || access_token

  @endpoint = endpoint.to_s.chomp("/")
  @auth = access_token ? ["Authorization", "Bearer #{access_token}"] : ["X-API-Key", api_key]
end

Class Method Details

.from_client_credentials(client_id:, client_secret:, scope: "puid:generate", endpoint: DEFAULT_ENDPOINT) ⇒ Object

Exchange OAuth2 client credentials for a bearer token and return a ready client. This is how an app generates ids on a team’s behalf without ever handling the team’s API key.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/puid.rb', line 72

def self.from_client_credentials(client_id:, client_secret:, scope: "puid:generate", endpoint: DEFAULT_ENDPOINT)
  if client_id.to_s.empty? || client_secret.to_s.empty?
    raise Error.new("client_id and client_secret are required", code: "invalid_client")
  end

  uri = URI("#{endpoint.to_s.chomp("/")}/oauth/token")
  req = Net::HTTP::Post.new(uri)
  req.set_form_data(grant_type: "client_credentials", client_id: client_id,
                    client_secret: client_secret, scope: scope)
  req["Accept"] = "application/json"
  res = request(uri, req, "token request")
  body = parse_json(res.body)
  unless res.is_a?(Net::HTTPSuccess) && body["access_token"]
    raise Error.new(body["error_description"] || body["error"] || "token request failed with HTTP #{res.code}",
                    status: res.code.to_i, code: body["error"])
  end

  new(access_token: body["access_token"], endpoint: endpoint)
end

.parse_json(str) ⇒ Object



99
100
101
102
103
# File 'lib/puid.rb', line 99

def self.parse_json(str)
  JSON.parse(str.to_s)
rescue JSON::ParserError
  {}
end

.request(uri, req, what) ⇒ Object

Perform an HTTP request, wrapping transport failures as Puid::Error.



93
94
95
96
97
# File 'lib/puid.rb', line 93

def self.request(uri, req, what)
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
rescue StandardError => e
  raise Error.new("#{what} to PUID failed: #{e.message}", code: "network_error")
end

Instance Method Details

#idObject

Generate a single id.



52
53
54
# File 'lib/puid.rb', line 52

def id
  ids(1).first
end

#ids(count = 1) ⇒ Object

Generate count ids (1–10). Returns an array of id strings.



43
44
45
46
47
48
49
# File 'lib/puid.rb', line 43

def ids(count = 1)
  unless count.is_a?(Integer) && count.between?(1, 10)
    raise Error.new("count must be between 1 and 10", code: "invalid_count")
  end

  get("/v1/ids?n=#{count}")["ids"]
end

#ordinal(puid) ⇒ Object

Decode a PUID back to the counter value it encodes. Ruby integers are arbitrary precision, so the 128-bit value fits.

Raises:



58
59
60
61
62
# File 'lib/puid.rb', line 58

def ordinal(puid)
  raise Error.new("puid must be a non-empty string", code: "invalid_puid") unless puid.is_a?(String) && !puid.empty?

  Integer(get("/v1/ordinal/#{CGI.escape(puid)}")["ordinal"])
end

#quotaObject

Today’s usage and remaining daily quota. Does not spend an id.



65
66
67
# File 'lib/puid.rb', line 65

def quota
  get("/v1/quota")
end