turbine_rb 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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ require "turbine_rb"
6
+
7
+ class MyApp
8
+ def call(app)
9
+ database = app.resource(name: "demopg")
10
+
11
+ # ELT pipeline example
12
+ # records = database.records(collection: 'events')
13
+ # database.write(records: records, collection: 'events_copy')
14
+
15
+ # procedural API
16
+ records = database.records(collection: "events")
17
+
18
+ # This register the secret to be available in the turbine application
19
+ app.register_secrets("MY_ENV_TEST")
20
+
21
+ # you can also register several secrets at once
22
+ # app.register_secrets(["MY_ENV_TEST", "MY_OTHER_ENV_TEST"])
23
+
24
+ # Passthrough just has to match the signature
25
+ processed_records = app.process(records: records, process: Passthrough.new)
26
+ database.write(records: processed_records, collection: "events_copy")
27
+
28
+ # out_records = processed_records.join(records, key: "user_id", window: 1.day) # stream joins
29
+
30
+ # chaining API
31
+ # database.records(collection: "events").
32
+ # process_with(process: Passthrough.new).
33
+ # write_to(resource: database, collection: "events_copy")
34
+ end
35
+ end
36
+
37
+ # might be useful to signal that this is a special Turbine call
38
+ class Passthrough < TurbineRb::Process
39
+ def call(records:)
40
+ puts "got records: #{records}"
41
+ # to get the value of unformatted records, use record .value getter method
42
+ # records.map { |r| puts r.value }
43
+ #
44
+ # to transform unformatted records, use record .value setter method
45
+ # records.map { |r| r.value = "newdata" }
46
+ #
47
+ # to get the value of json formatted records, use record .get method
48
+ # records.map { |r| puts r.get("message") }
49
+ #
50
+ # to transform json formatted records, use record .set methods
51
+ # records.map { |r| r.set('message', 'goodbye') }
52
+ records
53
+ end
54
+ end
55
+
56
+ TurbineRb.register(MyApp.new)
@@ -0,0 +1,9 @@
1
+ {
2
+ "events": [
3
+ {
4
+ "key": "1",
5
+ "value": {"message":"hello"},
6
+ "timestamp": "1662758822"
7
+ }
8
+ ]
9
+ }
File without changes
data/lib/turbine_pb.rb ADDED
@@ -0,0 +1,100 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # source: turbine.proto
3
+
4
+ require 'google/protobuf'
5
+
6
+ require 'google/protobuf/empty_pb'
7
+ require 'google/protobuf/timestamp_pb'
8
+ require 'google/protobuf/wrappers_pb'
9
+ require 'validate/validate_pb'
10
+
11
+ Google::Protobuf::DescriptorPool.generated_pool.build do
12
+ add_file("turbine.proto", :syntax => :proto3) do
13
+ add_message "turbine_core.InitRequest" do
14
+ optional :appName, :string, 1
15
+ optional :configFilePath, :string, 2
16
+ optional :language, :enum, 3, "turbine_core.Language"
17
+ optional :gitSHA, :string, 4
18
+ optional :turbineVersion, :string, 5
19
+ end
20
+ add_message "turbine_core.GetResourceRequest" do
21
+ optional :name, :string, 1
22
+ end
23
+ add_message "turbine_core.Resource" do
24
+ optional :name, :string, 1
25
+ end
26
+ add_message "turbine_core.Collection" do
27
+ optional :name, :string, 1
28
+ optional :stream, :string, 2
29
+ repeated :records, :message, 3, "turbine_core.Record"
30
+ end
31
+ add_message "turbine_core.Record" do
32
+ optional :key, :string, 1
33
+ optional :value, :bytes, 2
34
+ optional :timestamp, :message, 3, "google.protobuf.Timestamp"
35
+ end
36
+ add_message "turbine_core.ReadCollectionRequest" do
37
+ optional :resource, :message, 1, "turbine_core.Resource"
38
+ optional :collection, :string, 2
39
+ optional :configs, :message, 3, "turbine_core.Configs"
40
+ end
41
+ add_message "turbine_core.WriteCollectionRequest" do
42
+ optional :resource, :message, 1, "turbine_core.Resource"
43
+ optional :sourceCollection, :message, 2, "turbine_core.Collection"
44
+ optional :targetCollection, :string, 3
45
+ optional :configs, :message, 4, "turbine_core.Configs"
46
+ end
47
+ add_message "turbine_core.Configs" do
48
+ repeated :config, :message, 1, "turbine_core.Config"
49
+ end
50
+ add_message "turbine_core.Config" do
51
+ optional :field, :string, 1
52
+ optional :value, :string, 2
53
+ end
54
+ add_message "turbine_core.ProcessCollectionRequest" do
55
+ optional :process, :message, 1, "turbine_core.ProcessCollectionRequest.Process"
56
+ optional :collection, :message, 2, "turbine_core.Collection"
57
+ end
58
+ add_message "turbine_core.ProcessCollectionRequest.Process" do
59
+ optional :name, :string, 1
60
+ end
61
+ add_message "turbine_core.Secret" do
62
+ optional :name, :string, 1
63
+ optional :value, :string, 2
64
+ end
65
+ add_message "turbine_core.ListResourcesResponse" do
66
+ repeated :resources, :message, 1, "turbine_core.Resource"
67
+ end
68
+ add_message "turbine_core.GetSpecRequest" do
69
+ optional :image, :string, 1
70
+ end
71
+ add_message "turbine_core.GetSpecResponse" do
72
+ optional :spec, :bytes, 1
73
+ end
74
+ add_enum "turbine_core.Language" do
75
+ value :GOLANG, 0
76
+ value :PYTHON, 1
77
+ value :JAVASCRIPT, 2
78
+ value :RUBY, 3
79
+ end
80
+ end
81
+ end
82
+
83
+ module TurbineCore
84
+ InitRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.InitRequest").msgclass
85
+ GetResourceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.GetResourceRequest").msgclass
86
+ Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Resource").msgclass
87
+ Collection = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Collection").msgclass
88
+ Record = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Record").msgclass
89
+ ReadCollectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.ReadCollectionRequest").msgclass
90
+ WriteCollectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.WriteCollectionRequest").msgclass
91
+ Configs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Configs").msgclass
92
+ Config = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Config").msgclass
93
+ ProcessCollectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.ProcessCollectionRequest").msgclass
94
+ ProcessCollectionRequest::Process = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.ProcessCollectionRequest.Process").msgclass
95
+ Secret = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Secret").msgclass
96
+ ListResourcesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.ListResourcesResponse").msgclass
97
+ GetSpecRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.GetSpecRequest").msgclass
98
+ GetSpecResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.GetSpecResponse").msgclass
99
+ Language = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("turbine_core.Language").enummodule
100
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurbineRb
4
+ module Client
5
+ class MissingSecretError < StandardError; end
6
+
7
+ class App
8
+ attr_reader :core_server
9
+
10
+ def initialize(grpc_server, is_recording: false)
11
+ @core_server = grpc_server
12
+ @is_recording = is_recording
13
+ end
14
+
15
+ def resource(name:)
16
+ req = TurbineCore::GetResourceRequest.new(name: name)
17
+ res = @core_server.get_resource(req)
18
+ Resource.new(res, self)
19
+ end
20
+
21
+ def process(records:, process:)
22
+ unwrapped_records = records.unwrap if records.instance_of?(Collection)
23
+
24
+ pr = TurbineCore::ProcessCollectionRequest::Process.new(
25
+ name: process.class.name
26
+ )
27
+
28
+ req = TurbineCore::ProcessCollectionRequest.new(collection: unwrapped_records, process: pr)
29
+ @core_server.add_process_to_collection(req)
30
+ records.pb_collection = process.call(records: records.pb_collection) unless @is_recording
31
+
32
+ records
33
+ end
34
+
35
+ # register_secrets accepts either a single string or an array of strings
36
+ def register_secrets(secrets)
37
+ [*secrets].map do |secret|
38
+ raise MissingSecretError, "secret #{secret} is not an environment variable" unless ENV.key?(secret)
39
+
40
+ req = TurbineCore::Secret.new(name: secret, value: ENV[secret])
41
+ @core_server.register_secret(req)
42
+ end
43
+ end
44
+
45
+ class Resource
46
+ attr_reader :pb_resource
47
+
48
+ def initialize(res, app)
49
+ @pb_resource = res
50
+ @app = app
51
+ end
52
+
53
+ def records(collection:, configs: nil)
54
+ req = TurbineCore::ReadCollectionRequest.new(resource: @pb_resource, collection: collection)
55
+ if configs
56
+ pb_configs = configs.keys.map { |key| TurbineCore::Config.new(field: key, value: configs[key]) }
57
+ req.configs = TurbineCore::Configs.new(config: pb_configs)
58
+ end
59
+
60
+ @app.core_server.read_collection(req).wrap(@app) # wrap in Collection to enable chaining
61
+ end
62
+
63
+ def write(records:, collection:, configs: nil)
64
+ if records.instance_of?(Collection) # it has been processed by a function, so unwrap back to gRPC collection
65
+ records = records.unwrap
66
+ end
67
+
68
+ req = TurbineCore::WriteCollectionRequest.new(resource: @pb_resource, sourceCollection: records,
69
+ targetCollection: collection)
70
+
71
+ if configs
72
+ pb_configs = configs.keys.map { |key| TurbineCore::Config.new(field: key, value: configs[key]) }
73
+ req.configs = TurbineCore::Configs.new(config: pb_configs)
74
+ end
75
+
76
+ @app.core_server.write_collection_to_resource(req)
77
+ end
78
+ end
79
+
80
+ class Collection
81
+ attr_accessor :pb_collection, :pb_stream, :name
82
+
83
+ def initialize(name, collection, stream, app)
84
+ @name = name
85
+ @pb_collection = collection
86
+ @pb_stream = stream
87
+ @app = app
88
+ end
89
+
90
+ def write_to(resource:, collection:, configs: nil)
91
+ resource.write(records: self, collection: collection, configs: configs)
92
+ end
93
+
94
+ def process_with(process:)
95
+ @app.process(records: self, process: process)
96
+ end
97
+
98
+ def unwrap
99
+ TurbineCore::Collection.new( # convert back to TurbineCore::Collection
100
+ name: name,
101
+ records: pb_collection.to_a,
102
+ stream: pb_stream
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ TurbineCore::Collection.class_eval do
4
+ def wrap(app)
5
+ TurbineRb::Client::App::Collection.new(
6
+ name,
7
+ records,
8
+ stream,
9
+ app
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "hash_dot"
5
+
6
+ module TurbineRb
7
+ class Record
8
+ attr_accessor :key, :value, :timestamp
9
+
10
+ def initialize(pb_record)
11
+ @key = pb_record.key
12
+ @timestamp = pb_record.timestamp
13
+
14
+ begin
15
+ @value = JSON.parse(pb_record.value)
16
+ rescue JSON::ParserError
17
+ @value = pb_record.value
18
+ end
19
+
20
+ @value = @value.to_dot if value_hash?
21
+ end
22
+
23
+ def serialize
24
+ Io::Meroxa::Funtime::Record.new(key: @key, value: @value.to_json, timestamp: @timestamp)
25
+ end
26
+
27
+ def get(key)
28
+ if value_string? || value_array?
29
+ @value
30
+ elsif cdc_format?
31
+ @value.send("payload.after.#{key}")
32
+ else
33
+ @value.send("payload.#{key}")
34
+ end
35
+ end
36
+
37
+ def set(key, value)
38
+ if !value_hash?
39
+ @value = value
40
+ else
41
+ payload_key = cdc_format? ? "payload.after" : "payload"
42
+
43
+ begin
44
+ @value.send("#{payload_key}.#{key}")
45
+ rescue NoMethodError
46
+ schema = @value.send("schema.fields")
47
+ new_schema_field = { field: key, optional: true, type: "string" }.to_dot
48
+
49
+ if cdc_format?
50
+ schema_fields = schema.find { |f| f.field == "after" }
51
+ schema_fields.fields.unshift(new_schema_field)
52
+ else
53
+ schema.unshift(new_schema_field)
54
+ end
55
+ end
56
+
57
+ @value.send("#{payload_key}.#{key}=", value)
58
+ end
59
+ end
60
+
61
+ def unwrap!
62
+ return unless cdc_format?
63
+
64
+ payload = @value.send("payload")
65
+ schema = @value.send("schema.fields")
66
+ schema_fields = schema.find { |f| f.field == "after" }
67
+ unless schema_fields.nil?
68
+ schema_fields.delete("field")
69
+ schema_fields.name = @value.send("schema.name")
70
+ @value.send("schema=", schema_fields)
71
+ end
72
+
73
+ @value.send("payload=", payload.after)
74
+ end
75
+
76
+ private
77
+
78
+ def value_string?
79
+ @value.is_a?(String)
80
+ end
81
+
82
+ def value_array?
83
+ @value.is_a?(Array)
84
+ end
85
+
86
+ def value_hash?
87
+ @value.is_a?(Hash)
88
+ end
89
+
90
+ def json_schema?
91
+ value_hash? &&
92
+ @value.key?("payload") &&
93
+ @value.key?("schema")
94
+ end
95
+
96
+ def cdc_format?
97
+ json_schema? &&
98
+ @value.payload.key?("source")
99
+ end
100
+
101
+ def type_of_value(value)
102
+ case value
103
+ when String
104
+ "string"
105
+ when Integer
106
+ "int32"
107
+ when Float
108
+ "float32"
109
+ when true, false
110
+ "boolean"
111
+ else
112
+ "unsupported"
113
+ end
114
+ end
115
+ end
116
+
117
+ class Records < SimpleDelegator
118
+ def initialize(pb_records)
119
+ super
120
+ records = pb_records.map { |r| Record.new(r) }
121
+ __setobj__(records)
122
+ end
123
+
124
+ def unwrap!
125
+ records = __getobj__
126
+ records.each(&:unwrap!)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurbineRb
4
+ VERSION = "0.1.0"
5
+ end
data/lib/turbine_rb.rb ADDED
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "service_services_pb"
4
+ require "turbine_services_pb"
5
+
6
+ require "turbine_rb/collection_patch"
7
+ require "turbine_rb/version"
8
+ require "turbine_rb/client"
9
+ require "turbine_rb/records"
10
+
11
+ require "optparse"
12
+ require "fileutils"
13
+
14
+ require "grpc"
15
+ require "grpc/health/v1/health_pb"
16
+ require "grpc/health/checker"
17
+
18
+ module TurbineRb
19
+ class Error < StandardError; end
20
+
21
+ class << self
22
+ attr_reader :app, :process_klass
23
+
24
+ def register(app)
25
+ @app = app
26
+ end
27
+
28
+ def register_fn(fn_klass)
29
+ @process_klass = fn_klass
30
+ end
31
+
32
+ def serve
33
+ process_function = @process_klass.new
34
+ process_function_impl = ProcessImpl.new(process_function)
35
+ function_addr = ENV["MEROXA_FUNCTION_ADDR"] ||= "0.0.0.0:50500"
36
+
37
+ @grpc_server = GRPC::RpcServer.new
38
+ @grpc_server.add_http2_port(function_addr, :this_port_is_insecure)
39
+ @grpc_server.handle(process_function_impl)
40
+ @grpc_server.handle(HealthCheck)
41
+ puts "serving function #{process_function.class.name} on #{function_addr}"
42
+ @grpc_server.run_till_terminated_or_interrupted([1, "int", "SIGQUIT"])
43
+ end
44
+
45
+ def run
46
+ app = TurbineRb::Client::App.new(init_core_server)
47
+ TurbineRb.app.call(app)
48
+ end
49
+
50
+ def record
51
+ app = TurbineRb::Client::App.new(init_core_server, is_recording: true)
52
+ TurbineRb.app.call(app)
53
+ end
54
+
55
+ def build
56
+ docker_file = File.join(__dir__, "templates", "Dockerfile")
57
+ dest_app = Dir.getwd
58
+ FileUtils.cp(docker_file, dest_app)
59
+ end
60
+
61
+ private
62
+
63
+ def init_core_server
64
+ # TODO: figure out what the deal is with :this_channel_is_insecure
65
+ core_server = TurbineCore::TurbineService::Stub.new(ENV["TURBINE_CORE_SERVER"], :this_channel_is_insecure)
66
+ git_sha = ARGV[0]
67
+
68
+ req = TurbineCore::InitRequest.new(
69
+ appName: app.class.name,
70
+ configFilePath: Dir.getwd,
71
+ language: :RUBY,
72
+ gitSHA: git_sha,
73
+ turbineVersion: Gem.loaded_specs["turbine_rb"].version.version
74
+ )
75
+
76
+ core_server.init(req)
77
+ core_server
78
+ end
79
+ end
80
+
81
+ class ProcessImpl < Io::Meroxa::Funtime::Function::Service
82
+ def initialize(process)
83
+ @process = process
84
+ super()
85
+ end
86
+
87
+ def process(request, _call)
88
+ records = TurbineRb::Records.new(request.records)
89
+
90
+ # records are processed but not in proto format
91
+ processed_records = @process.call(records: records)
92
+
93
+ # to proto
94
+ serialized_records = processed_records.map(&:serialize)
95
+
96
+ Io::Meroxa::Funtime::ProcessRecordResponse.new(records: serialized_records)
97
+ end
98
+ end
99
+
100
+ class Process
101
+ def self.inherited(subclass)
102
+ TurbineRb.register_fn(subclass)
103
+ super
104
+ end
105
+ end
106
+
107
+ class HealthCheck < Grpc::Health::V1::Health::Service
108
+ def check(req, req_view)
109
+ checker = Grpc::Health::Checker.new
110
+ checker.set_status_for_services(
111
+ Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING,
112
+ "function"
113
+ )
114
+ checker.check(req, req_view)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,30 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: turbine.proto for package 'turbine_core'
3
+
4
+ require 'grpc'
5
+ require 'turbine_pb'
6
+
7
+ module TurbineCore
8
+ module TurbineService
9
+ class Service
10
+
11
+ include ::GRPC::GenericService
12
+
13
+ self.marshal_class_method = :encode
14
+ self.unmarshal_class_method = :decode
15
+ self.service_name = 'turbine_core.TurbineService'
16
+
17
+ rpc :Init, ::TurbineCore::InitRequest, ::Google::Protobuf::Empty
18
+ rpc :GetResource, ::TurbineCore::GetResourceRequest, ::TurbineCore::Resource
19
+ rpc :ReadCollection, ::TurbineCore::ReadCollectionRequest, ::TurbineCore::Collection
20
+ rpc :WriteCollectionToResource, ::TurbineCore::WriteCollectionRequest, ::Google::Protobuf::Empty
21
+ rpc :AddProcessToCollection, ::TurbineCore::ProcessCollectionRequest, ::TurbineCore::Collection
22
+ rpc :RegisterSecret, ::TurbineCore::Secret, ::Google::Protobuf::Empty
23
+ rpc :HasFunctions, ::Google::Protobuf::Empty, ::Google::Protobuf::BoolValue
24
+ rpc :ListResources, ::Google::Protobuf::Empty, ::TurbineCore::ListResourcesResponse
25
+ rpc :GetSpec, ::TurbineCore::GetSpecRequest, ::TurbineCore::GetSpecResponse
26
+ end
27
+
28
+ Stub = Service.rpc_stub_class
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ # This file is a place holder, do not remove.
2
+ # The plugin used for `validate/validate.proto` does not support ruby, however
3
+ # the `require` file in the emitted code references it even though it doesn't exist.
@@ -0,0 +1,4 @@
1
+ module TurbineRb
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: turbine_rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Meroxa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: grpc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.48'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.48'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hash_dot
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.5.0
41
+ description: |
42
+ Turbine is a data application framework for building
43
+ server-side applications that are event-driven,
44
+ respond to data in real-time, and scale using cloud-native best practices
45
+ email:
46
+ - production@meroxa.io
47
+ executables:
48
+ - turbine-function
49
+ - turbine-build
50
+ - turbine-record
51
+ - turbine-run
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - ".rspec"
56
+ - ".rubocop.yml"
57
+ - CHANGELOG.md
58
+ - CODE_OF_CONDUCT.md
59
+ - Gemfile
60
+ - Gemfile.lock
61
+ - Guardfile
62
+ - LICENSE.txt
63
+ - README.md
64
+ - Rakefile
65
+ - bin/turbine-build
66
+ - bin/turbine-function
67
+ - bin/turbine-record
68
+ - bin/turbine-run
69
+ - lib/service_pb.rb
70
+ - lib/service_services_pb.rb
71
+ - lib/templates/Dockerfile
72
+ - lib/templates/app/Gemfile
73
+ - lib/templates/app/app.json
74
+ - lib/templates/app/app.rb
75
+ - lib/templates/app/fixtures/demo.json
76
+ - lib/templates/app/ignoregit
77
+ - lib/turbine_pb.rb
78
+ - lib/turbine_rb.rb
79
+ - lib/turbine_rb/client.rb
80
+ - lib/turbine_rb/collection_patch.rb
81
+ - lib/turbine_rb/records.rb
82
+ - lib/turbine_rb/version.rb
83
+ - lib/turbine_services_pb.rb
84
+ - lib/validate/validate_pb.rb
85
+ - sig/turbine_framework.rbs
86
+ homepage: https://github.com/meroxa/turbine-core/tree/main/lib/ruby/turbine_rb
87
+ licenses:
88
+ - LicenseRef-LICENSE.txt
89
+ metadata:
90
+ homepage_uri: https://github.com/meroxa/turbine-core/tree/main/lib/ruby/turbine_rb
91
+ source_code_uri: https://github.com/meroxa/turbine-core
92
+ changelog_uri: https://github.com/meroxa/turbine-core
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.6.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.3.7
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Meroxa data application framework for Ruby
112
+ test_files: []