heedkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +57 -0
- data/lib/heedkit/changelog.rb +55 -0
- data/lib/heedkit/client.rb +115 -0
- data/lib/heedkit/railtie.rb +19 -0
- data/lib/heedkit/roadmap.rb +45 -0
- data/lib/heedkit/version.rb +5 -0
- data/lib/heedkit.rb +54 -0
- metadata +54 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d90db8da72e11a9058298022e35575c16bb5ddc99ed82e99f311cd337b017f36
|
|
4
|
+
data.tar.gz: c6bb8675fad6cba884df875a86617ad9fb2df24a822662fbf58ce20cc94632b4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 548f044f2276926ebe16960f0da8969dab328e9546edb392e6344dc208fbd56458034f0284585349b78a9b09a3d56ea2169a166bb35bdadd46081d808258ad8c
|
|
7
|
+
data.tar.gz: 4429b95f990e9065fc41569cc8b2fd59dcaf5d0bedb2d4533ddf401c2086d1103741b63b7f0c163c48f8a1577431229b12d03d6e3298ba8146bd14cb69e9f45f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HeedKit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# heedkit (Ruby / Rails SDK)
|
|
2
|
+
|
|
3
|
+
Server-side SDK for [HeedKit](https://heedkit.com). Fetch a project's **public
|
|
4
|
+
roadmap** and drive the **end-user feedback API** (identify / list / submit / vote /
|
|
5
|
+
comment) from any Ruby or Rails app.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "heedkit"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
HeedKit.configure do |c|
|
|
15
|
+
c.project_key = ENV["HEEDKIT_PROJECT_KEY"] # "fk_..."
|
|
16
|
+
c.endpoint = "https://acme.heedkit.com" # your HeedKit base URL
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Roadmap
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
roadmap = HeedKit.roadmap # => HeedKit::Roadmap
|
|
24
|
+
roadmap.project_name # "Acme Feedback"
|
|
25
|
+
roadmap.each_column do |status, label, items|
|
|
26
|
+
puts "#{label}: #{items.map(&:title).join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In **Rails**, a `heedkit` helper (the configured client) is available in controllers
|
|
31
|
+
and views:
|
|
32
|
+
|
|
33
|
+
```erb
|
|
34
|
+
<% heedkit.roadmap.each_column do |status, label, items| %>
|
|
35
|
+
<h3><%= label %></h3>
|
|
36
|
+
<% items.each { |item| %><p><%= item.title %> · ▲ <%= item.vote_count %></p><% } %>
|
|
37
|
+
<% end %>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Feedback API
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
fk = HeedKit.client
|
|
44
|
+
user = fk.identify(external_id: "user-123", email: "ada@example.com")
|
|
45
|
+
fk.submit(end_user_id: user["end_user_id"], title: "Dark mode", kind: "feature_request")
|
|
46
|
+
fk.features(end_user_id: user["end_user_id"], sort: "top")
|
|
47
|
+
fk.vote(feature_id, end_user_id: user["end_user_id"])
|
|
48
|
+
fk.comment(feature_id, end_user_id: user["end_user_id"], body: "Yes please")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`identify` / `submit` / `features` / `vote` / `comment` authenticate with the project key
|
|
52
|
+
via the `X-Project-Key` header; `roadmap` reads the public endpoint. All methods raise
|
|
53
|
+
`HeedKit::Error` on a non-2xx response.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module HeedKit
|
|
6
|
+
# A single published changelog entry (release note). `body` is markdown; `published_at`
|
|
7
|
+
# is a Time (or nil).
|
|
8
|
+
ChangelogEntry = Struct.new(
|
|
9
|
+
:id, :title, :body, :category, :category_label, :published_at, keyword_init: true
|
|
10
|
+
) do
|
|
11
|
+
def date = published_at
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# A project's public changelog: published entries, newest first.
|
|
15
|
+
class Changelog
|
|
16
|
+
CATEGORY_LABELS = {
|
|
17
|
+
"new" => "New", "improved" => "Improved", "fixed" => "Fixed", "announcement" => "Announcement"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :project_name, :theme, :entries
|
|
21
|
+
|
|
22
|
+
def self.from_payload(payload)
|
|
23
|
+
entries = (payload["entries"] || []).map do |e|
|
|
24
|
+
ChangelogEntry.new(
|
|
25
|
+
id: e["id"], title: e["title"], body: e["body"], category: e["category"],
|
|
26
|
+
category_label: e["category_label"] || CATEGORY_LABELS[e["category"]] || e["category"],
|
|
27
|
+
published_at: parse_time(e["published_at"])
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
new(project_name: payload["project_name"], theme: payload["theme"] || {}, entries: entries)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.parse_time(value)
|
|
34
|
+
return nil if value.to_s.empty?
|
|
35
|
+
Time.iso8601(value.to_s)
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(project_name:, theme:, entries:)
|
|
41
|
+
@project_name = project_name
|
|
42
|
+
@theme = theme
|
|
43
|
+
@entries = entries
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
include Enumerable
|
|
47
|
+
def each(&block) = entries.each(&block)
|
|
48
|
+
|
|
49
|
+
def primary_color
|
|
50
|
+
theme["primary"] || "#0d9488"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size = entries.size
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module HeedKit
|
|
8
|
+
# Server-side client for the HeedKit API. Talks to the public roadmap endpoint
|
|
9
|
+
# and the end-user SDK endpoints (X-Project-Key auth).
|
|
10
|
+
class Client
|
|
11
|
+
DEFAULT_TIMEOUT = 5
|
|
12
|
+
|
|
13
|
+
attr_reader :project_key, :endpoint
|
|
14
|
+
|
|
15
|
+
def initialize(project_key:, endpoint:, timeout: DEFAULT_TIMEOUT)
|
|
16
|
+
raise ArgumentError, "project_key is required" if project_key.to_s.empty?
|
|
17
|
+
raise ArgumentError, "endpoint is required" if endpoint.to_s.empty?
|
|
18
|
+
|
|
19
|
+
@project_key = project_key
|
|
20
|
+
@endpoint = endpoint.to_s.chomp("/")
|
|
21
|
+
@timeout = timeout
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET the public roadmap. Returns a HeedKit::Roadmap.
|
|
25
|
+
def roadmap
|
|
26
|
+
Roadmap.from_payload(get("/public/projects/#{project_key}/roadmap"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# GET the public changelog. Returns a HeedKit::Changelog.
|
|
30
|
+
def changelog
|
|
31
|
+
Changelog.from_payload(get("/public/projects/#{project_key}/changelog"))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# POST /sdk/init — identify (find-or-create) an end-user. Returns the parsed body
|
|
35
|
+
# ({ "end_user_id" => ..., "project" => {...} }).
|
|
36
|
+
def identify(external_id: nil, email: nil, name: nil, avatar_url: nil, platform: nil)
|
|
37
|
+
sdk_post("/sdk/init", external_id:, email:, name:, avatar_url:, platform:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# GET /sdk/features — public features plus the caller's own private submissions.
|
|
41
|
+
def features(end_user_id: nil, status: nil, kind: nil, sort: "top", cursor: nil)
|
|
42
|
+
query = { end_user_id:, status:, kind:, sort:, cursor: }.compact
|
|
43
|
+
sdk_get("/sdk/features", query)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# POST /sdk/features — submit a feature on behalf of an end-user.
|
|
47
|
+
def submit(end_user_id:, title:, description: nil, kind: "feature_request", tag: nil)
|
|
48
|
+
sdk_post("/sdk/features", end_user_id:, title:, description:, kind:, tag:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# POST /sdk/features/:id/vote — toggle a vote.
|
|
52
|
+
def vote(feature_id, end_user_id:)
|
|
53
|
+
sdk_post("/sdk/features/#{feature_id}/vote", end_user_id:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# POST /sdk/features/:id/comments — comment as an end-user.
|
|
57
|
+
def comment(feature_id, end_user_id:, body:)
|
|
58
|
+
sdk_post("/sdk/features/#{feature_id}/comments", end_user_id:, body:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def sdk_get(path, query)
|
|
64
|
+
get(path, query:, headers: key_header)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sdk_post(path, **body)
|
|
68
|
+
post(path, body: body.compact, headers: key_header)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def key_header
|
|
72
|
+
{ "X-Project-Key" => project_key }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get(path, query: {}, headers: {})
|
|
76
|
+
uri = build_uri(path, query)
|
|
77
|
+
request(Net::HTTP::Get.new(uri), uri, headers)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def post(path, body:, headers: {})
|
|
81
|
+
uri = build_uri(path)
|
|
82
|
+
req = Net::HTTP::Post.new(uri)
|
|
83
|
+
req["Content-Type"] = "application/json"
|
|
84
|
+
req.body = JSON.generate(body)
|
|
85
|
+
request(req, uri, headers)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_uri(path, query = {})
|
|
89
|
+
uri = URI.parse("#{endpoint}#{path}")
|
|
90
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
91
|
+
uri
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def request(req, uri, headers)
|
|
95
|
+
headers.each { |k, v| req[k] = v }
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.use_ssl = uri.scheme == "https"
|
|
98
|
+
http.open_timeout = @timeout
|
|
99
|
+
http.read_timeout = @timeout
|
|
100
|
+
|
|
101
|
+
res = http.request(req)
|
|
102
|
+
unless res.code.to_i.between?(200, 299)
|
|
103
|
+
raise Error, "HeedKit API #{res.code} for #{uri.path}: #{res.body}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
res.body.to_s.empty? ? {} : JSON.parse(res.body)
|
|
107
|
+
rescue JSON::ParserError => e
|
|
108
|
+
raise Error, "Invalid JSON from HeedKit API: #{e.message}"
|
|
109
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => e
|
|
110
|
+
# Wrap transport failures (connection refused, DNS, timeouts) in our own error
|
|
111
|
+
# type so callers only need to rescue HeedKit::Error.
|
|
112
|
+
raise Error, "HeedKit request to #{uri} failed: #{e.message}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeedKit
|
|
4
|
+
# Exposes a `heedkit` accessor (the configured client) to controllers and views.
|
|
5
|
+
module Helper
|
|
6
|
+
def heedkit
|
|
7
|
+
HeedKit.client
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
initializer "heedkit.helper" do
|
|
13
|
+
ActiveSupport.on_load(:action_controller) do
|
|
14
|
+
include HeedKit::Helper
|
|
15
|
+
helper HeedKit::Helper if respond_to?(:helper)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeedKit
|
|
4
|
+
# A single roadmap item.
|
|
5
|
+
RoadmapItem = Struct.new(:id, :title, :description, :vote_count, :tag, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
# The public roadmap for a project: ordered status columns, each a list of items.
|
|
8
|
+
class Roadmap
|
|
9
|
+
# Column order matches the product (Planned → In progress → Shipped).
|
|
10
|
+
STATUSES = %w[planned in_progress shipped].freeze
|
|
11
|
+
LABELS = { "planned" => "Planned", "in_progress" => "In progress", "shipped" => "Shipped" }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :project_name, :theme, :columns
|
|
14
|
+
|
|
15
|
+
def self.from_payload(payload)
|
|
16
|
+
cols = (payload["columns"] || {}).transform_values do |items|
|
|
17
|
+
items.map do |i|
|
|
18
|
+
RoadmapItem.new(id: i["id"], title: i["title"], description: i["description"],
|
|
19
|
+
vote_count: i["vote_count"], tag: i["tag"])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
new(project_name: payload["project_name"], theme: payload["theme"] || {}, columns: cols)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(project_name:, theme:, columns:)
|
|
26
|
+
@project_name = project_name
|
|
27
|
+
@theme = theme
|
|
28
|
+
@columns = columns
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each_column
|
|
32
|
+
return enum_for(:each_column) unless block_given?
|
|
33
|
+
|
|
34
|
+
STATUSES.each { |status| yield status, LABELS[status], (columns[status] || []) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def primary_color
|
|
38
|
+
theme["primary"] || "#0d9488"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def total
|
|
42
|
+
columns.values.sum(&:size)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/heedkit.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "heedkit/version"
|
|
4
|
+
require "heedkit/roadmap"
|
|
5
|
+
require "heedkit/changelog"
|
|
6
|
+
require "heedkit/client"
|
|
7
|
+
|
|
8
|
+
# Ruby / Rails SDK for HeedKit — fetch a project's public roadmap and drive the
|
|
9
|
+
# end-user feedback API (identify / list / submit / vote / comment) from your server.
|
|
10
|
+
#
|
|
11
|
+
# HeedKit.configure do |c|
|
|
12
|
+
# c.project_key = "fk_..."
|
|
13
|
+
# c.endpoint = "https://acme.heedkit.com" # your HeedKit base URL
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# HeedKit.roadmap # => HeedKit::Roadmap
|
|
17
|
+
# HeedKit.client.identify(external_id: "u-1")
|
|
18
|
+
module HeedKit
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
class Configuration
|
|
22
|
+
attr_accessor :project_key, :endpoint, :timeout
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@endpoint = "https://api.heedkit.com"
|
|
26
|
+
@timeout = 5
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def configuration
|
|
32
|
+
@configuration ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
yield configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A client built from the global configuration.
|
|
40
|
+
def client
|
|
41
|
+
Client.new(project_key: configuration.project_key, endpoint: configuration.endpoint, timeout: configuration.timeout)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def roadmap
|
|
45
|
+
client.roadmap
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def changelog
|
|
49
|
+
client.changelog
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
require "heedkit/railtie" if defined?(::Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: heedkit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- HeedKit
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Fetch a project's public roadmap and drive the end-user feedback API
|
|
13
|
+
(identify / list / submit / vote / comment) from your Ruby or Rails server.
|
|
14
|
+
email:
|
|
15
|
+
- support@heedkit.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/heedkit.rb
|
|
23
|
+
- lib/heedkit/changelog.rb
|
|
24
|
+
- lib/heedkit/client.rb
|
|
25
|
+
- lib/heedkit/railtie.rb
|
|
26
|
+
- lib/heedkit/roadmap.rb
|
|
27
|
+
- lib/heedkit/version.rb
|
|
28
|
+
homepage: https://heedkit.com
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://heedkit.com
|
|
33
|
+
source_code_uri: https://github.com/heedkit/heedkit-sdk-ruby
|
|
34
|
+
bug_tracker_uri: https://github.com/heedkit/heedkit-sdk-ruby/issues
|
|
35
|
+
allowed_push_host: https://rubygems.org
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.1'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 4.0.6
|
|
52
|
+
specification_version: 4
|
|
53
|
+
summary: Ruby / Rails SDK for HeedKit — public roadmap + feedback API
|
|
54
|
+
test_files: []
|