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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HeedKit
4
+ VERSION = "0.1.0"
5
+ 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: []