proc 0.0.2 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59becc885513c542ccee8100b436c2854d5c42939b72424a691a281741861728
4
- data.tar.gz: 82cfac2a503a9e934efeae9b41d037b6dd72d1e78e493a6ec921d04a39fec97e
3
+ metadata.gz: 2950f8e2178a95a40c29372c902203d883bda75ac84785a3579134b9394d7a52
4
+ data.tar.gz: 6c8433c3f49bbc0a6a62187c9b992fd7c39e6eac2894f7d71a2056205bdc7900
5
5
  SHA512:
6
- metadata.gz: 71cbb386c7e906584a706538f3cccba04a7090496486b80160b2856b2d4dbe480d35b02f83814643fb208173518053f283b9d4e2c365f8164781e569b23f98ea
7
- data.tar.gz: '095adc172bb26b52871a0fb9d8265fb782cab0ae43981c03f1bedfe47200cdf1551e78cf1564e29ade46a535f0c717350e306513583b4ced7e09eac815a10c73'
6
+ metadata.gz: 36828988724d7e3cad08a3a741f576bfa50f8d53a459062892bb270e01da1d8a1ddf4977255976a314a15b4e79e8266da4089c9cdbb8f723c30d681099654c7f
7
+ data.tar.gz: e224df7dcabcf284d5cf72f410be3ccf713dfbb0ff386ae5a50597e5c9a6b07634c87759f30d5e4b4a95fc0091d0c2f0aa490146a02b8806a3784033025fd123
@@ -0,0 +1,80 @@
1
+ The `proc` gem lets you call codeless web functions through the proc.dev service.
2
+
3
+ ## Getting Started
4
+
5
+ Install `proc` using the `gem` command from the command line:
6
+
7
+ ```
8
+ gem install proc
9
+ ```
10
+
11
+ You will also need a proc.dev account. Create a free account in seconds at [proc.dev](https://proc.dev/).
12
+
13
+ ## Connecting
14
+
15
+ Sign in to your proc.dev account and locate your secret key. Use the secret to create a client connection:
16
+
17
+ ```ruby
18
+ client = Proc.connect("secret-key")
19
+ ```
20
+
21
+ ## Calling Procs
22
+
23
+ You can call available procs through a connected client:
24
+
25
+ ```ruby
26
+ client.core.string.reverse.call("hello")
27
+ => "olleh"
28
+ ```
29
+
30
+ Requests are sent to proc.dev anytime the `call` method is invoked.
31
+
32
+ ## Callable Contexts
33
+
34
+ Proc lookups create contexts that can be called later:
35
+
36
+ ```ruby
37
+ string = client.core.string
38
+
39
+ string.reverse.call("hello")
40
+ => "olleh"
41
+
42
+ string.truncate.call("hello", length: 3)
43
+ => "hel"
44
+ ```
45
+
46
+ Contexts can be configured with default input and arguments using the `with` method:
47
+
48
+ ```ruby
49
+ truncate = client.core.string.truncate.with("default", length: 1)
50
+
51
+ truncate.call
52
+ => "d"
53
+ ```
54
+
55
+ Default input and/or arguments can be overidden for a specific call:
56
+
57
+ ```ruby
58
+ truncate.call(length: 3)
59
+ => "def"
60
+ ```
61
+
62
+ Procs can also be looked up using the hash key syntax:
63
+
64
+ ```ruby
65
+ client["core.string.truncate"].call("hello", length: 3)
66
+ => "hel"
67
+ ```
68
+
69
+ ## Compositions
70
+
71
+ Procs can be composed together to build more complex behavior:
72
+
73
+ ```ruby
74
+ composition = client.core.string.reverse >> client.core.string.truncate(length: 3) >> client.core.string.capitalize
75
+
76
+ composition.call("hello")
77
+ => "Oll"
78
+ ```
79
+
80
+ Compositions are sent to proc.dev in a single request.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proc
4
+ class Argument
5
+ def initialize(name, **options)
6
+ @name = name
7
+ @options = options
8
+ end
9
+
10
+ def serialize
11
+ {"::" => {"name" => @name.to_s}.merge(serialized_options)}
12
+ end
13
+
14
+ def serialized_options
15
+ @options.each_pair.each_with_object({}) { |(key, value), hash|
16
+ hash[key.to_s] = serialize_value(value)
17
+ }
18
+ end
19
+
20
+ private def serialize_value(value)
21
+ if value.respond_to?(:serialize)
22
+ value.serialize
23
+ else
24
+ value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,22 +2,104 @@
2
2
 
3
3
  class Proc
4
4
  class Callable
5
- attr_reader :proc
5
+ attr_reader :proc, :input, :arguments
6
6
 
7
- def initialize(proc, client:)
7
+ def initialize(proc, client:, input: nil, arguments: {})
8
8
  @proc = proc
9
9
  @client = client
10
+ @input = input
11
+ @arguments = arguments
10
12
  end
11
13
 
12
- def call(input = nil, **arguments)
13
- @client.perform(@proc, input, **arguments)
14
+ def initialize_copy(_)
15
+ @input = input.dup
16
+ @arguments = arguments.dup
14
17
  end
15
18
 
16
- def >>(callable)
17
- composed = Composition.new(client: @client)
19
+ def call(input = input_omitted = true, **arguments)
20
+ callable = self.class.new(
21
+ @proc,
22
+ client: @client,
23
+ input: input_omitted ? @input : input,
24
+ arguments: @arguments.merge(arguments)
25
+ )
26
+
27
+ @client.call(@proc, callable.serialized_input, **callable.serialized_arguments)
28
+ end
29
+
30
+ def with(input = input_omitted = true, **arguments)
31
+ self.class.new(
32
+ @proc,
33
+ client: @client,
34
+ input: input_omitted ? @input : input,
35
+ arguments: @arguments.merge(arguments)
36
+ )
37
+ end
38
+
39
+ def >>(other)
40
+ composed = Composition.new(client: @client, input: @input)
18
41
  composed << self
19
- composed << callable
42
+ composed << other
20
43
  composed
21
44
  end
45
+
46
+ def serialize
47
+ {
48
+ "{}" => {
49
+ "<<" => serialized_input,
50
+ "[]" => [[@proc, serialized_arguments]]
51
+ }
52
+ }
53
+ end
54
+
55
+ def serialized_input
56
+ serialize_value(@input)
57
+ end
58
+
59
+ def serialized_arguments
60
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
61
+ hash[key.to_s] = serialize_value(value)
62
+ }
63
+ end
64
+
65
+ def [](proc)
66
+ Callable.new(
67
+ [@proc, proc].join("."),
68
+ client: @client,
69
+ input: @input,
70
+ arguments: @arguments
71
+ )
72
+ end
73
+
74
+ IGNORE_MISSING = %i[to_hash].freeze
75
+
76
+ def method_missing(name, input = input_omitted = true, **arguments)
77
+ if IGNORE_MISSING.include?(name)
78
+ super
79
+ else
80
+ Callable.new(
81
+ [@proc, name].join("."),
82
+ client: @client,
83
+ input: input_omitted ? @input : input,
84
+ arguments: @arguments.merge(arguments)
85
+ )
86
+ end
87
+ end
88
+
89
+ def respond_to_missing?(name, *)
90
+ if IGNORE_MISSING.include?(name)
91
+ super
92
+ else
93
+ true
94
+ end
95
+ end
96
+
97
+ private def serialize_value(value)
98
+ if value.respond_to?(:serialize)
99
+ value.serialize
100
+ else
101
+ value
102
+ end
103
+ end
22
104
  end
23
105
  end
@@ -3,6 +3,7 @@
3
3
  require "async"
4
4
  require "oj"
5
5
 
6
+ require_relative "argument"
6
7
  require_relative "callable"
7
8
  require_relative "composition"
8
9
  require_relative "null_logger"
@@ -13,7 +14,7 @@ class Proc
13
14
  class Error < StandardError
14
15
  end
15
16
 
16
- class ArgumentError < ::ArgumentError
17
+ class Invalid < ::ArgumentError
17
18
  end
18
19
 
19
20
  class Undefined < ::NameError
@@ -22,9 +23,18 @@ class Proc
22
23
  class Unauthorized < Error
23
24
  end
24
25
 
26
+ class Forbidden < Error
27
+ end
28
+
25
29
  class Unavailable < Error
26
30
  end
27
31
 
32
+ class Limited < Error
33
+ end
34
+
35
+ class Timeout < Error
36
+ end
37
+
28
38
  class Client < Http::Client
29
39
  def initialize(authorization, scheme: "https", host: "proc.dev")
30
40
  @authorization = authorization
@@ -38,29 +48,49 @@ class Proc
38
48
  Callable.new(proc, client: self)
39
49
  end
40
50
 
41
- def compose(&block)
42
- Composition.new(client: self, &block)
51
+ def remaining
52
+ unless defined?(@remaining)
53
+ self["ping"].call
54
+ end
55
+
56
+ @remaining
57
+ end
58
+
59
+ def resets_at
60
+ unless defined?(@resets_at)
61
+ self["ping"].call
62
+ end
63
+
64
+ @resets_at
43
65
  end
44
66
 
45
67
  DEFAULT_HEADERS = {
68
+ "accept" => "application/json",
46
69
  "content-type" => "application/json"
47
70
  }.freeze
48
71
 
49
- def perform(proc, input, **arguments)
72
+ def call(proc = nil, input = nil, **arguments)
50
73
  Async(logger: NullLogger) { |task|
51
- body = {
52
- input: input, arguments: arguments
53
- }
74
+ body = { "<<" => serialize_value(input) }
75
+
76
+ arguments.each_pair do |key, value|
77
+ body[key.to_s] = serialize_value(value)
78
+ end
54
79
 
55
80
  headers = {
56
- "authorization" => "Bearer #{@authorization}"
81
+ "authorization" => "bearer #{@authorization}"
57
82
  }.merge(DEFAULT_HEADERS)
58
83
 
59
84
  begin
60
- response = call(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :json), task: task)
85
+ response = super(:post, build_uri(proc), headers: headers, body: Oj.dump(body, mode: :custom), task: task)
61
86
 
62
- payload = Oj.load(response.read, mode: :strict)
87
+ @remaining = response.headers["x-rate-limit-remaining"].to_s.to_i
88
+ @resets_at = Time.at(response.headers["x-rate-limit-reset"].to_s.to_i)
89
+
90
+ payload = Oj.load(response.read, mode: :compat)
63
91
  rescue => error
92
+ # TODO: This should wrap `error`.
93
+ #
64
94
  raise Proc::Unavailable, error.message
65
95
  ensure
66
96
  response&.close
@@ -68,23 +98,54 @@ class Proc
68
98
 
69
99
  case response.status
70
100
  when 200
71
- payload["value"]
101
+ payload[">>"]
72
102
  when 400
73
- raise Proc::ArgumentError, payload.dig("error", "message")
74
- when 403
103
+ raise Proc::Invalid, payload.dig("error", "message")
104
+ when 401
75
105
  raise Proc::Unauthorized, payload.dig("error", "message")
106
+ when 403
107
+ raise Proc::Forbidden, payload.dig("error", "message")
76
108
  when 404
77
109
  raise Proc::Undefined, payload.dig("error", "message")
110
+ when 408
111
+ raise Proc::Timeout, payload.dig("error", "message")
112
+ when 429
113
+ raise Proc::Limited, payload.dig("error", "message")
78
114
  when 500
79
115
  raise Proc::Error, payload.dig("error", "message")
116
+ when 508
117
+ raise Proc::Error, payload.dig("error", "message")
118
+ else
119
+ raise Proc::Error, "unhandled"
80
120
  end
81
121
  }.wait
82
122
  end
83
123
 
124
+ def method_missing(name, input = nil, **arguments)
125
+ Callable.new(name, client: self, input: input, arguments: arguments)
126
+ end
127
+
128
+ def respond_to_missing?(name, *)
129
+ true
130
+ end
131
+
132
+ def argument(name, **options)
133
+ Argument.new(name, **options)
134
+ end
135
+ alias_method :arg, :argument
136
+
84
137
  private def build_uri(proc)
85
- host_and_path = File.join(@host, proc.split(".").join("/"))
138
+ host_and_path = File.join(@host, proc.to_s.split(".").join("/"))
86
139
 
87
140
  "#{@scheme}://#{host_and_path}"
88
141
  end
142
+
143
+ private def serialize_value(value)
144
+ if value.respond_to?(:serialize)
145
+ value.serialize
146
+ else
147
+ value
148
+ end
149
+ end
89
150
  end
90
151
  end
@@ -2,31 +2,49 @@
2
2
 
3
3
  class Proc
4
4
  class Composition
5
- require_relative "composition/deferable"
6
- require_relative "composition/evaluator"
5
+ attr_reader :input, :callables, :arguments
7
6
 
8
- def initialize(client:, &block)
7
+ def initialize(client:, input:, callables: [], arguments: {})
9
8
  @client = client
10
- @block = block
11
- @callables = []
9
+ @input = input
10
+ @callables = callables
11
+ @arguments = arguments
12
12
  end
13
13
 
14
14
  def initialize_copy(_)
15
15
  @callables = @callables.dup
16
16
  end
17
17
 
18
- def call(input, **arguments)
19
- if @block
20
- evaluator = @block.call(Evaluator.new, **arguments)
21
- @client.perform("compose", input, procs: evaluator.serialize.concat(serialize))
22
- else
23
- @client.perform("compose", input, procs: serialize)
24
- end
18
+ def call(input = input_omitted = true, **arguments)
19
+ callable = self.class.new(
20
+ client: @client,
21
+ input: input_omitted ? @input : input,
22
+ callables: @callables.dup,
23
+ arguments: @arguments.merge(arguments)
24
+ )
25
+
26
+ @client.call("proc.exec", nil, proc: callable.serialize)
25
27
  end
26
28
 
27
- def >>(callable)
29
+ def with(input = input_omitted = true, **arguments)
30
+ self.class.new(
31
+ client: @client,
32
+ input: input_omitted ? @input : input,
33
+ callables: @callables.dup,
34
+ arguments: @arguments.merge(arguments)
35
+ )
36
+ end
37
+
38
+ def >>(other)
28
39
  composed = dup
29
- composed << callable
40
+
41
+ case other
42
+ when Composition
43
+ composed.merge(other)
44
+ when Callable
45
+ composed << other
46
+ end
47
+
30
48
  composed
31
49
  end
32
50
 
@@ -34,8 +52,44 @@ class Proc
34
52
  @callables << callable
35
53
  end
36
54
 
37
- protected def serialize
38
- @callables.map(&:proc)
55
+ def serialize
56
+ {
57
+ "{}" => {
58
+ "<<" => serialized_input,
59
+ "[]" => serialized_calls
60
+ }.merge(serialized_arguments)
61
+ }
62
+ end
63
+
64
+ def serialized_calls
65
+ @callables.map { |callable|
66
+ [callable.proc, callable.serialized_arguments]
67
+ }
68
+ end
69
+
70
+ def serialized_input
71
+ serialize_value(@input)
72
+ end
73
+
74
+ def serialized_arguments
75
+ @arguments.each_pair.each_with_object({}) { |(key, value), hash|
76
+ hash[key.to_s] = serialize_value(value)
77
+ }
78
+ end
79
+
80
+ def merge(composition)
81
+ raise ArgumentError, "expected a composition" unless composition.is_a?(self.class)
82
+
83
+ @callables.concat(composition.callables)
84
+ @arguments.merge!(composition.arguments)
85
+ end
86
+
87
+ private def serialize_value(value)
88
+ if value.respond_to?(:serialize)
89
+ value.serialize
90
+ else
91
+ value
92
+ end
39
93
  end
40
94
  end
41
95
  end
@@ -14,7 +14,7 @@ class Proc
14
14
  end
15
15
 
16
16
  def callable
17
- return @method.to_s.upcase, finalize_uri(@uri, @params), @headers, @body
17
+ [@method.to_s.upcase, finalize_uri(@uri, @params), @headers, @body]
18
18
  end
19
19
 
20
20
  private def finalize_uri(uri, params)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Proc
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.2"
5
5
 
6
6
  def self.version
7
7
  VERSION
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Powell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2020-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -45,13 +45,12 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - LICENSE
48
- - lib/examples/scratch.rb
48
+ - README.md
49
49
  - lib/proc.rb
50
+ - lib/proc/argument.rb
50
51
  - lib/proc/callable.rb
51
52
  - lib/proc/client.rb
52
53
  - lib/proc/composition.rb
53
- - lib/proc/composition/deferable.rb
54
- - lib/proc/composition/evaluator.rb
55
54
  - lib/proc/http/client.rb
56
55
  - lib/proc/http/request.rb
57
56
  - lib/proc/http/response.rb
@@ -1,61 +0,0 @@
1
- # Pass `token: "..."` or default to `PROC_ACCESS_TOKEN`:
2
- #
3
- client = Proc.connect
4
-
5
- # Not chainable, returns the value:
6
- #
7
- client.stdlib.echo("bar")
8
- # => bar
9
-
10
- # Create chains using `chain` (perhaps alias as `with`):
11
- #
12
- client.chain("foo") { |chain| chain.stdlib.reverse.capitalize.truncate(2) }
13
- # => Oo
14
-
15
- # Or return a chain to pass around:
16
- #
17
- chain = client.chain("foo")
18
- # => <Proc::Chain ...>
19
- chain.stdlib.echo("bar")
20
-
21
- # Call `value` or `perform` to evaluate the chain:
22
- #
23
- chain.value
24
-
25
- # Calling a non-existent proc raises a Proc::NameError,
26
- #
27
- chain.nonexistent
28
-
29
- # Pass values through different contexts:
30
- #
31
- client.chain("foo") { |chain| chain.stdlib.reverse >> chain.whatever.other }
32
-
33
- # Use from the cli (included with the ruby gem OR as a go library):
34
- #
35
- # proc echo "bar" -t access-token
36
- # proc chain "bar" "stdlib.reverse >> whatever.other" -t access-token
37
-
38
- #########################################################
39
- # alternatives so that we don't have to fetch metadata: #
40
- #########################################################
41
-
42
- proc = Proc.connect
43
-
44
- # I like this because it's obvious how to get a reference as well as just make the call.
45
- #
46
- proc["stdlib.echo"].call("foo")
47
-
48
- # This works for simple composition, where the value is simply passed through.
49
- #
50
- my_proc = proc["stdlib.reverse"] >> proc["stdlib.capitalize"]
51
-
52
- my_proc.call("foo")
53
-
54
- # Perhaps something like this, where we compose a proc with arguments. Each is implicitly called, or
55
- # call explicitly to pass an argument to the proc. I sort of like the simplicity of this.
56
- #
57
- my_proc = proc.compose { |context, length:|
58
- context["stdlib.reverse"] >> context["stdlib.capitalize"] >> context["stdlib.truncate"].call(length)
59
- }
60
-
61
- my_proc.call("foo", length: 2)
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Proc
4
- class Composition
5
- class Deferable
6
- attr_reader :proc
7
- attr_writer :arguments
8
-
9
- def initialize(proc)
10
- @proc = proc
11
- @arguments = {}
12
- end
13
-
14
- def serialize
15
- [@proc, @arguments]
16
- end
17
- end
18
- end
19
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Proc
4
- class Composition
5
- class Evaluator
6
- attr_reader :callables
7
-
8
- def initialize
9
- @callables = []
10
- end
11
-
12
- def initialize_copy(_)
13
- @callables = @callables.map(&:dup)
14
- end
15
-
16
- def [](proc)
17
- evaluator = dup
18
- evaluator << Deferable.new(proc)
19
- evaluator
20
- end
21
-
22
- def <<(proc)
23
- @callables << proc
24
- end
25
-
26
- def >>(callable)
27
- evaluator = dup
28
-
29
- case callable
30
- when Evaluator
31
- callable.callables.each do |each_callable|
32
- evaluator << each_callable
33
- end
34
- when Callable
35
- evaluator << Deferable.new(callable.proc)
36
- end
37
-
38
- evaluator
39
- end
40
-
41
- def call(**arguments)
42
- evaluator = dup
43
- evaluator.callables.last.arguments = arguments
44
- evaluator
45
- end
46
-
47
- def serialize
48
- @callables.map(&:serialize)
49
- end
50
- end
51
- end
52
- end