tana 0.0.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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tana
4
+ # The primary interface for making API requests.
5
+ class Client
6
+ include Import[:configuration]
7
+
8
+ def initialize(endpoint: Endpoints::Add::Action.new, **)
9
+ @endpoint = endpoint
10
+ super(**)
11
+ yield configuration if block_given?
12
+ end
13
+
14
+ def add(...) = endpoint.call(...)
15
+
16
+ private
17
+
18
+ attr_reader :endpoint
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tana
4
+ module Configuration
5
+ # Defines the content of the client configuration for API requests.
6
+ Content = Struct.new :accept, :token, :url
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "refinements/string"
4
+
5
+ module Tana
6
+ module Configuration
7
+ # Handles loading of configuration with environment defaults.
8
+ class Loader
9
+ using Refinements::String
10
+
11
+ def initialize model = Content, environment: ENV
12
+ @model = model
13
+ @environment = environment
14
+ end
15
+
16
+ def call
17
+ model[
18
+ accept: environment.fetch("TANA_API_ACCEPT", "application/json"),
19
+ token: environment["TANA_API_TOKEN"],
20
+ url: environment.fetch("TANA_API_URL", "https://europe-west1-tagr-prod.cloudfunctions.net")
21
+ ]
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :model, :environment
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-container"
4
+ require "http"
5
+
6
+ module Tana
7
+ # Defines application dependencies.
8
+ module Container
9
+ extend Dry::Container::Mixin
10
+
11
+ register(:configuration, memoize: true) { Configuration::Loader.new.call }
12
+ register(:http) { HTTP }
13
+ register(:client, memoize: true) { API::Client.new }
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "transactable"
4
+
5
+ module Tana
6
+ module Endpoints
7
+ module Add
8
+ # Handles a repository create action.
9
+ class Action
10
+ include Import[:client]
11
+ include Transactable
12
+
13
+ # rubocop:todo Metrics/ParameterLists
14
+ def initialize(
15
+ request: Requests::Add.new,
16
+ response: Responses::Root,
17
+ model: Models::Root,
18
+ **
19
+ )
20
+ @request = request
21
+ @response = response
22
+ @model = model
23
+ super(**)
24
+ end
25
+ # rubocop:enable Metrics/ParameterLists
26
+
27
+ def call(body, **)
28
+ pipe body,
29
+ validate(request),
30
+ insert("addToNodeV2", at: 0),
31
+ insert(**),
32
+ to(client, :post),
33
+ try(:parse, catch: JSON::ParserError),
34
+ validate(response),
35
+ to(model, :for)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :request, :response, :model
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "infusible"
4
+
5
+ module Tana
6
+ Import = Infusible.with Container
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "core"
4
+
5
+ module Tana
6
+ module Models
7
+ # A generic node.
8
+ Node = Data.define :id, :name, :description, :type, :children do
9
+ def self.for(**attributes) = new(**attributes.transform_keys!(nodeId: :id))
10
+
11
+ def initialize id:, type:, name: nil, description: nil, children: Core::EMPTY_ARRAY
12
+ updated_children = children.map { |child| self.class.for(**child) }
13
+ super id:, name:, type:, description:, children: updated_children
14
+ end
15
+
16
+ alias_method :to_hash, :to_h
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "core"
4
+
5
+ module Tana
6
+ module Models
7
+ # A root node.
8
+ Root = Data.define :children do
9
+ def self.for(node: Node, **attributes)
10
+ attributes.fetch(:children, Core::EMPTY_ARRAY)
11
+ .map { |child| node.for(**child) }
12
+ .then { |children| new children: }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation"
4
+ require "refinements/array"
5
+
6
+ module Tana
7
+ module Requests
8
+ # Validates an API add request.
9
+ class Add < Dry::Validation::Contract
10
+ using Refinements::Array
11
+
12
+ TARGETS = ["INBOX", "SCHEMA", /\A[0-9a-z\-_]{12}\Z/i].freeze
13
+
14
+ def initialize(targets: TARGETS, **)
15
+ @targets = targets
16
+ super(**)
17
+ end
18
+
19
+ json do
20
+ optional(:targetNodeId).filled :string
21
+ required(:nodes).value(:array, min_size?: 1).each { hash LevelA }
22
+ end
23
+
24
+ rule :targetNodeId do
25
+ next unless key? && targets.none? { |target| value.match? target }
26
+
27
+ key.failure "is invalid. Use: #{targets.to_usage :or}"
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :targets
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the first level of an API request.
8
+ LevelA = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelB }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the second level of an API request.
8
+ LevelB = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelC }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the third level of an API request.
8
+ LevelC = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelD }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the fourth level of an API request.
8
+ LevelD = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelE }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the fifth level of an API request.
8
+ LevelE = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelF }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the sixth level of an API request.
8
+ LevelF = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelG }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the seventh level of an API request.
8
+ LevelG = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelH }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the eight level of an API request.
8
+ LevelH = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelI }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Requests
7
+ # Validates the ninth level of an API request.
8
+ LevelI = Dry::Schema.JSON parent: Node.schema do
9
+ optional(:children).value(:array, min_size?: 1).each { hash Node.schema }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation"
4
+ require "refinements/array"
5
+
6
+ module Tana
7
+ module Requests
8
+ # Validates a generic node within an API request.
9
+ class Node < Dry::Validation::Contract
10
+ TYPES = %w[boolean date field file plain reference url].freeze
11
+
12
+ using Refinements::Array
13
+
14
+ def initialize(types: TYPES, **)
15
+ @types = types
16
+ super(**)
17
+ end
18
+
19
+ json do
20
+ optional(:attributeId).filled :string
21
+ optional(:contentType).filled :string
22
+ optional(:dataType).filled :string
23
+ optional(:description).filled :string
24
+ optional(:file).filled :string
25
+ optional(:filename).filled :string
26
+ optional(:id).filled :string
27
+ optional(:name).filled :string
28
+ optional(:supertags).array(:hash) { required(:id).filled :string }
29
+ optional(:type).filled :string
30
+ optional(:value).filled :bool
31
+ end
32
+
33
+ rule :type do
34
+ key.failure "is invalid. Use: #{types.to_usage :or}" if key? && !types.include?(value)
35
+ end
36
+
37
+ rule :dataType do
38
+ key.failure "is invalid. Use: #{types.to_usage :or}" if key? && !types.include?(value)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :types
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the first level of an API response.
8
+ LevelA = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelB }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the second level of an API response.
8
+ LevelB = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelC }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the third level of an API response.
8
+ LevelC = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelD }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the fourth level of an API response.
8
+ LevelD = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelE }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the fifth level of an API response.
8
+ LevelE = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelF }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the sixth level of an API response.
8
+ LevelF = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelG }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the seventh level of an API response.
8
+ LevelG = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelH }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the eighth level of an API response.
8
+ LevelH = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelI }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the ninth level of an API response.
8
+ LevelI = Dry::Schema.JSON parent: Node do
9
+ optional(:children).value(:array, min_size?: 1).each { hash Node }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates a generic node within an API response.
8
+ Node = Dry::Schema.JSON do
9
+ optional(:description).filled :string
10
+ optional(:name).filled :string
11
+ required(:nodeId).filled :string
12
+ required(:type).filled :string
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+
5
+ module Tana
6
+ module Responses
7
+ # Validates the root node of an API response.
8
+ Root = Dry::Schema.JSON do
9
+ optional(:children).value(:array, min_size?: 1).each { hash LevelA }
10
+ end
11
+ end
12
+ end
data/lib/tana.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+ require "dry/validation"
5
+ require "zeitwerk"
6
+
7
+ Dry::Schema.load_extensions :monads
8
+ Dry::Validation.load_extensions :monads
9
+
10
+ Zeitwerk::Loader.new.then do |loader|
11
+ loader.inflector.inflect "api" => "API"
12
+ loader.tag = File.basename __FILE__, ".rb"
13
+ loader.push_dir __dir__
14
+ loader.setup
15
+ end
16
+
17
+ # Main namespace.
18
+ module Tana
19
+ def self.loader registry = Zeitwerk::Registry
20
+ @loader ||= registry.loaders.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
21
+ end
22
+
23
+ def self.new(...) = Client.new(...)
24
+ end
data/tana.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "tana"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://alchemists.io/projects/tana"
9
+ spec.summary = "A monadic API client for the Tana Personal Knowledge Management system."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/tana/issues",
14
+ "changelog_uri" => "https://alchemists.io/projects/tana/versions",
15
+ "documentation_uri" => "https://alchemists.io/projects/tana",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "Tana",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/tana"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = "~> 3.3"
26
+ spec.add_dependency "core", "~> 1.0"
27
+ spec.add_dependency "dry-container", "~> 0.11"
28
+ spec.add_dependency "dry-monads", "~> 1.6"
29
+ spec.add_dependency "dry-validation", "~> 1.10"
30
+ spec.add_dependency "http", "~> 5.1"
31
+ spec.add_dependency "infusible", "~> 3.0"
32
+ spec.add_dependency "refinements", "~> 12.0"
33
+ spec.add_dependency "transactable", "~> 0.10"
34
+ spec.add_dependency "zeitwerk", "~> 2.6"
35
+
36
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
37
+ spec.files = Dir["*.gemspec", "lib/**/*"]
38
+ end
data.tar.gz.sig ADDED
Binary file