smily_cli 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.
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SmilyCli
6
+ # A normalized view over one or more API v3 responses. It pulls the record
7
+ # array out of the JSON:API envelope (the single top-level key that isn't
8
+ # `links`/`linked`/`meta`), while keeping the raw envelope, `meta`, sideloaded
9
+ # `linked` records, pagination relations and rate-limit headers available for
10
+ # formatters and verbose output.
11
+ class Result
12
+ SPECIAL_KEYS = %w[links linked meta].freeze
13
+
14
+ # @return [Array<Hash>] the resource records (always an array)
15
+ attr_reader :records
16
+ # @return [Hash, nil] the raw parsed envelope of the last response
17
+ attr_reader :envelope
18
+ # @return [Hash, nil] JSON:API `meta`
19
+ attr_reader :meta
20
+ # @return [Hash, nil] sideloaded associations (`linked`)
21
+ attr_reader :linked
22
+ # @return [Integer, nil] HTTP status of the last response
23
+ attr_reader :status
24
+ # @return [Hash] response headers of the last response
25
+ attr_reader :headers
26
+ # @return [String, nil] resolved JSON:API key the records live under
27
+ attr_reader :key
28
+
29
+ # @param records [Array<Hash>]
30
+ # @param single [Boolean] whether this represents one member (get/create)
31
+ # @param envelope [Hash, nil]
32
+ # @param key [String, nil]
33
+ # @param status [Integer, nil]
34
+ # @param headers [Hash]
35
+ def initialize(records:, single: false, envelope: nil, key: nil, status: nil, headers: {})
36
+ @records = records
37
+ @single = single
38
+ @envelope = envelope
39
+ @key = key
40
+ @meta = envelope.is_a?(Hash) ? envelope["meta"] : nil
41
+ @linked = envelope.is_a?(Hash) ? envelope["linked"] : nil
42
+ @status = status
43
+ @headers = headers || {}
44
+ end
45
+
46
+ # Build a Result from a single {BookingSync::API::Response}.
47
+ #
48
+ # @param response [BookingSync::API::Response, nil] nil for 204 No Content
49
+ # @param single [Boolean]
50
+ # @return [Result]
51
+ def self.from_response(response, single: false)
52
+ return empty(single: single) if response.nil?
53
+
54
+ body = parse(response.body)
55
+ key = resource_key(body)
56
+ records = extract_records(body, key)
57
+ new(
58
+ records: records,
59
+ single: single,
60
+ envelope: body,
61
+ key: key,
62
+ status: response.status,
63
+ headers: response.headers
64
+ )
65
+ end
66
+
67
+ # An empty result (e.g. a 204 delete). `records` is empty.
68
+ #
69
+ # @return [Result]
70
+ def self.empty(single: false, status: nil, headers: {})
71
+ new(records: [], single: single, envelope: nil, key: nil, status: status, headers: headers)
72
+ end
73
+
74
+ # Build a Result from an already-parsed JSON:API envelope (e.g. the payload
75
+ # an MCP tool returns), rather than an HTTP response.
76
+ #
77
+ # @param body [Hash]
78
+ # @param single [Boolean]
79
+ # @return [Result]
80
+ def self.from_body(body, single: false)
81
+ key = resource_key(body)
82
+ new(records: extract_records(body, key), single: single, envelope: body, key: key)
83
+ end
84
+
85
+ # @return [Boolean] whether this result represents a single member
86
+ def single?
87
+ @single
88
+ end
89
+
90
+ # @return [Hash, nil] the sole record, when {#single?}
91
+ def record
92
+ records.first
93
+ end
94
+
95
+ # @return [Boolean]
96
+ def empty?
97
+ records.empty?
98
+ end
99
+
100
+ # @return [Integer]
101
+ def count
102
+ records.length
103
+ end
104
+
105
+ # Remaining requests in the current rate-limit window, if reported.
106
+ #
107
+ # @return [Integer, nil]
108
+ def rate_limit_remaining
109
+ value = headers["X-RateLimit-Remaining"] || headers["x-ratelimit-remaining"]
110
+ value&.to_i
111
+ end
112
+
113
+ # Total number of pages reported by the API, if any.
114
+ #
115
+ # @return [Integer, nil]
116
+ def total_pages
117
+ value = headers["X-Total-Pages"] || headers["x-total-pages"]
118
+ value&.to_i
119
+ end
120
+
121
+ # @api private
122
+ def self.parse(raw)
123
+ return {} if raw.nil? || raw.to_s.empty?
124
+
125
+ JSON.parse(raw)
126
+ rescue JSON::ParserError
127
+ {}
128
+ end
129
+
130
+ # @api private
131
+ def self.resource_key(body)
132
+ return nil unless body.is_a?(Hash)
133
+
134
+ (body.keys.map(&:to_s) - SPECIAL_KEYS).first
135
+ end
136
+
137
+ # @api private
138
+ def self.extract_records(body, key)
139
+ return [] unless body.is_a?(Hash) && key
140
+
141
+ # v3 always wraps records in an array, but tolerate a bare object too.
142
+ case (value = body[key])
143
+ when Array then value
144
+ when nil then []
145
+ else [value]
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmilyCli
4
+ VERSION = "0.1.0"
5
+ end
data/lib/smily_cli.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ require_relative "smily_cli/version"
6
+ require_relative "smily_cli/errors"
7
+
8
+ # SmilyCli is a command-line client for the BookingSync (Smily) API v3.
9
+ #
10
+ # The library layer (everything under {SmilyCli}) is deliberately usable on its
11
+ # own, without Thor: build a {SmilyCli::Context}, hand it to a
12
+ # {SmilyCli::Client}, and render results with {SmilyCli::Formatters}. The Thor
13
+ # command layer ({SmilyCli::CLI} and friends) autoloads lazily, so requiring the
14
+ # library does not pull in Thor until the command classes are actually touched.
15
+ module SmilyCli
16
+ # The Thor-powered command-line interface. Referencing the constant autoloads
17
+ # (and thereby `require`s Thor); kept behind a method so the entry point reads
18
+ # as `SmilyCli.cli.start`.
19
+ #
20
+ # @return [Class] the {SmilyCli::CLI} Thor class
21
+ def self.cli
22
+ CLI
23
+ end
24
+ end
25
+
26
+ # Autoload everything under lib/smily_cli/**. version.rb and errors.rb hold a
27
+ # bare constant and a multi-class hierarchy respectively (neither maps 1:1 to a
28
+ # file name), so they are required eagerly above and ignored by the loader.
29
+ loader = Zeitwerk::Loader.for_gem
30
+ loader.inflector.inflect(
31
+ "cli" => "CLI",
32
+ "cli_options" => "CLIOptions",
33
+ "oauth" => "OAuth"
34
+ )
35
+ loader.ignore("#{__dir__}/smily_cli/version.rb")
36
+ loader.ignore("#{__dir__}/smily_cli/errors.rb")
37
+ loader.setup
data/sig/smily_cli.rbs ADDED
@@ -0,0 +1,21 @@
1
+ module SmilyCli
2
+ VERSION: String
3
+
4
+ def self.cli: () -> untyped
5
+
6
+ class Error < StandardError
7
+ def exit_status: () -> Integer
8
+ end
9
+
10
+ class ConfigurationError < Error
11
+ end
12
+
13
+ class UsageError < Error
14
+ end
15
+
16
+ class ApiError < Error
17
+ attr_reader status: Integer?
18
+ attr_reader body: untyped
19
+ attr_reader headers: Hash[String, untyped]
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smily_cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Karol Galanciak
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bookingsync-api
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.2'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: zeitwerk
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '2.6'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '2.6'
60
+ - !ruby/object:Gem::Dependency
61
+ name: base64
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: csv
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ type: :runtime
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ description: |
89
+ smily is an ergonomic, comprehensive command-line client for the BookingSync
90
+ (Smily) API v3. It offers per-resource commands (list/get/create/update/delete)
91
+ for the documented endpoints, a raw `api` escape hatch for anything else,
92
+ OAuth token helpers, config profiles, pagination, and table/json/yaml/csv/ndjson
93
+ output. Built on the official bookingsync-api client.
94
+ email:
95
+ - karol.galanciak@gmail.com
96
+ executables:
97
+ - smily
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - ".github/workflows/ci.yml"
102
+ - ".rubocop.yml"
103
+ - CHANGELOG.md
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - exe/smily
108
+ - lib/smily_cli.rb
109
+ - lib/smily_cli/base_command.rb
110
+ - lib/smily_cli/cli.rb
111
+ - lib/smily_cli/cli_options.rb
112
+ - lib/smily_cli/client.rb
113
+ - lib/smily_cli/color.rb
114
+ - lib/smily_cli/commands/auth_command.rb
115
+ - lib/smily_cli/commands/config_command.rb
116
+ - lib/smily_cli/commands/mcp_command.rb
117
+ - lib/smily_cli/completion.rb
118
+ - lib/smily_cli/config.rb
119
+ - lib/smily_cli/context.rb
120
+ - lib/smily_cli/errors.rb
121
+ - lib/smily_cli/formatters.rb
122
+ - lib/smily_cli/mcp/client.rb
123
+ - lib/smily_cli/oauth.rb
124
+ - lib/smily_cli/query_options.rb
125
+ - lib/smily_cli/redaction.rb
126
+ - lib/smily_cli/registry.rb
127
+ - lib/smily_cli/resource.rb
128
+ - lib/smily_cli/resource_command.rb
129
+ - lib/smily_cli/result.rb
130
+ - lib/smily_cli/version.rb
131
+ - sig/smily_cli.rbs
132
+ homepage: https://github.com/BookingSync/smily_cli
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ homepage_uri: https://github.com/BookingSync/smily_cli
137
+ source_code_uri: https://github.com/BookingSync/smily_cli
138
+ changelog_uri: https://github.com/BookingSync/smily_cli/blob/master/CHANGELOG.md
139
+ rubygems_mfa_required: 'true'
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: 3.2.0
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubygems_version: 3.6.9
155
+ specification_version: 4
156
+ summary: Command-line client for the BookingSync (Smily) API v3
157
+ test_files: []