facera 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 +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gem-push.yml +42 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +137 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +309 -0
- data/Rakefile +6 -0
- data/examples/01_core_dsl.rb +132 -0
- data/examples/02_facet_system.rb +216 -0
- data/examples/03_api_generation.rb +117 -0
- data/examples/04_auto_mounting.rb +182 -0
- data/examples/05_adapters.rb +196 -0
- data/examples/README.md +184 -0
- data/examples/server/README.md +376 -0
- data/examples/server/adapters/payment_adapter.rb +139 -0
- data/examples/server/application.rb +17 -0
- data/examples/server/config/facera.rb +33 -0
- data/examples/server/config.ru +10 -0
- data/examples/server/cores/payment_core.rb +82 -0
- data/examples/server/facets/external_facet.rb +38 -0
- data/examples/server/facets/internal_facet.rb +33 -0
- data/examples/server/facets/operator_facet.rb +48 -0
- data/facera.gemspec +30 -0
- data/img/facera.png +0 -0
- data/lib/facera/adapter.rb +83 -0
- data/lib/facera/attribute.rb +95 -0
- data/lib/facera/auto_mount.rb +124 -0
- data/lib/facera/capability.rb +117 -0
- data/lib/facera/capability_access.rb +59 -0
- data/lib/facera/configuration.rb +83 -0
- data/lib/facera/context.rb +29 -0
- data/lib/facera/core.rb +65 -0
- data/lib/facera/dsl.rb +41 -0
- data/lib/facera/entity.rb +50 -0
- data/lib/facera/error_formatter.rb +100 -0
- data/lib/facera/errors.rb +40 -0
- data/lib/facera/executor.rb +265 -0
- data/lib/facera/facet.rb +103 -0
- data/lib/facera/field_visibility.rb +69 -0
- data/lib/facera/generators/core_generator.rb +23 -0
- data/lib/facera/generators/facet_generator.rb +25 -0
- data/lib/facera/generators/install_generator.rb +64 -0
- data/lib/facera/generators/templates/core.rb.tt +49 -0
- data/lib/facera/generators/templates/facet.rb.tt +23 -0
- data/lib/facera/grape/api_generator.rb +59 -0
- data/lib/facera/grape/endpoint_generator.rb +316 -0
- data/lib/facera/grape/entity_generator.rb +89 -0
- data/lib/facera/grape.rb +14 -0
- data/lib/facera/introspection.rb +111 -0
- data/lib/facera/introspection_api.rb +66 -0
- data/lib/facera/invariant.rb +26 -0
- data/lib/facera/loader.rb +153 -0
- data/lib/facera/openapi_generator.rb +338 -0
- data/lib/facera/railtie.rb +51 -0
- data/lib/facera/registry.rb +34 -0
- data/lib/facera/tasks/routes.rake +66 -0
- data/lib/facera/version.rb +3 -0
- data/lib/facera.rb +35 -0
- metadata +137 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Internal Facet
|
|
2
|
+
# Service-to-service API for internal microservices
|
|
3
|
+
# Full access to all fields and operations
|
|
4
|
+
|
|
5
|
+
Facera.define_facet(:internal, core: :payment) do
|
|
6
|
+
description "Internal service-to-service API"
|
|
7
|
+
|
|
8
|
+
# Expose all fields for internal services
|
|
9
|
+
expose :payment do
|
|
10
|
+
fields :all
|
|
11
|
+
|
|
12
|
+
# Add computed fields for internal use
|
|
13
|
+
computed :age_in_seconds do
|
|
14
|
+
created_at ? (Time.now - created_at).to_i : 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
computed :is_recent do
|
|
18
|
+
age_in_seconds < 3600 # Less than 1 hour old
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Allow all capabilities
|
|
23
|
+
allow_capabilities :all
|
|
24
|
+
|
|
25
|
+
# No scoping - internal services can see everything
|
|
26
|
+
# They're trusted and may need to operate on any payment
|
|
27
|
+
|
|
28
|
+
# Detailed errors for debugging
|
|
29
|
+
error_verbosity :detailed
|
|
30
|
+
|
|
31
|
+
# Audit all internal operations (in production, this would log to a service)
|
|
32
|
+
audit_all_operations user: :system
|
|
33
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Operator Facet
|
|
2
|
+
# Admin/support operator API with enhanced visibility
|
|
3
|
+
# Used by support tools and admin dashboards
|
|
4
|
+
|
|
5
|
+
Facera.define_facet(:operator, core: :payment) do
|
|
6
|
+
description "Support operator and admin tools API"
|
|
7
|
+
|
|
8
|
+
# Expose all fields plus additional operational data
|
|
9
|
+
expose :payment do
|
|
10
|
+
fields :all
|
|
11
|
+
|
|
12
|
+
# Add operator-specific computed fields
|
|
13
|
+
computed :customer_name do
|
|
14
|
+
# In a real app: Customer.find(customer_id).name
|
|
15
|
+
"Customer #{customer_id[0..7]}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
computed :merchant_name do
|
|
19
|
+
# In a real app: Merchant.find(merchant_id).name
|
|
20
|
+
"Merchant #{merchant_id[0..7]}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
computed :time_in_current_state do
|
|
24
|
+
if status == :confirmed && confirmed_at
|
|
25
|
+
Time.now - confirmed_at
|
|
26
|
+
elsif status == :cancelled && cancelled_at
|
|
27
|
+
Time.now - cancelled_at
|
|
28
|
+
elsif created_at
|
|
29
|
+
Time.now - created_at
|
|
30
|
+
else
|
|
31
|
+
0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Allow all capabilities (operators can do everything)
|
|
37
|
+
allow_capabilities :all
|
|
38
|
+
|
|
39
|
+
# No scoping - operators need to see all payments for support
|
|
40
|
+
# But we audit all their actions
|
|
41
|
+
audit_all_operations user: :current_operator
|
|
42
|
+
|
|
43
|
+
# Detailed errors for troubleshooting
|
|
44
|
+
error_verbosity :detailed
|
|
45
|
+
|
|
46
|
+
# Structured format for better tooling integration
|
|
47
|
+
format :structured
|
|
48
|
+
end
|
data/facera.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative 'lib/facera/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "facera"
|
|
5
|
+
spec.version = Facera::VERSION
|
|
6
|
+
spec.authors = ["Juan Carlos Garcia"]
|
|
7
|
+
spec.email = ["jugade92@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "A Ruby framework for building multi-facet APIs from a single semantic core"
|
|
10
|
+
spec.description = "Facera allows you to define your system once as a semantic core and expose it through multiple facets, each tailored to different consumers while remaining logically consistent."
|
|
11
|
+
spec.homepage = "https://github.com/jcagarcia/facera"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
spec.bindir = "exe"
|
|
24
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
# Runtime dependencies
|
|
28
|
+
spec.add_dependency "grape", "~> 2.0"
|
|
29
|
+
spec.add_dependency "grape-entity", "~> 1.0"
|
|
30
|
+
end
|
data/img/facera.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
# Base module for adapters
|
|
3
|
+
# Adapters implement the actual business logic for capabilities
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
# class PaymentAdapter
|
|
7
|
+
# include Facera::Adapter
|
|
8
|
+
#
|
|
9
|
+
# def create_payment(params)
|
|
10
|
+
# Payment.create!(params)
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# def get_payment(id:)
|
|
14
|
+
# Payment.find(id)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
module Adapter
|
|
18
|
+
def self.included(base)
|
|
19
|
+
base.extend(ClassMethods)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Returns the core name this adapter is for
|
|
24
|
+
# Inferred from class name: PaymentAdapter -> :payment
|
|
25
|
+
def core_name
|
|
26
|
+
name.gsub(/Adapter$/, '').split('::').last.underscore.to_sym
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Default implementation for capabilities
|
|
31
|
+
# Subclasses override specific methods
|
|
32
|
+
|
|
33
|
+
def create(params)
|
|
34
|
+
raise NotImplementedError, "#{self.class}#create not implemented"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(id:)
|
|
38
|
+
raise NotImplementedError, "#{self.class}#get not implemented"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def list(filters = {})
|
|
42
|
+
raise NotImplementedError, "#{self.class}#list not implemented"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def action(capability_name, params)
|
|
46
|
+
raise NotImplementedError, "#{self.class}#action(#{capability_name}) not implemented"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Adapter registry
|
|
51
|
+
class AdapterRegistry
|
|
52
|
+
@adapters = {}
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
def register(core_name, adapter_class)
|
|
56
|
+
@adapters[core_name.to_sym] = adapter_class
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get(core_name)
|
|
60
|
+
@adapters[core_name.to_sym]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def all
|
|
64
|
+
@adapters
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clear!
|
|
68
|
+
@adapters = {}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# String underscore helper
|
|
75
|
+
class String
|
|
76
|
+
def underscore
|
|
77
|
+
gsub(/::/, '/')
|
|
78
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
79
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
80
|
+
.tr('-', '_')
|
|
81
|
+
.downcase
|
|
82
|
+
end unless method_defined?(:underscore)
|
|
83
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class Attribute
|
|
3
|
+
attr_reader :name, :type, :options
|
|
4
|
+
|
|
5
|
+
VALID_TYPES = [
|
|
6
|
+
:string,
|
|
7
|
+
:integer,
|
|
8
|
+
:float,
|
|
9
|
+
:boolean,
|
|
10
|
+
:uuid,
|
|
11
|
+
:money,
|
|
12
|
+
:timestamp,
|
|
13
|
+
:datetime,
|
|
14
|
+
:date,
|
|
15
|
+
:enum,
|
|
16
|
+
:hash,
|
|
17
|
+
:array
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(name, type, **options)
|
|
21
|
+
@name = name.to_sym
|
|
22
|
+
@type = type.to_sym
|
|
23
|
+
@options = options
|
|
24
|
+
|
|
25
|
+
validate_type!
|
|
26
|
+
validate_options!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def required?
|
|
30
|
+
@options[:required] == true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def immutable?
|
|
34
|
+
@options[:immutable] == true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def default_value
|
|
38
|
+
@options[:default]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enum_values
|
|
42
|
+
@options[:values] || []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_value(value)
|
|
46
|
+
return true if value.nil? && !required?
|
|
47
|
+
return false if value.nil? && required?
|
|
48
|
+
|
|
49
|
+
case type
|
|
50
|
+
when :string
|
|
51
|
+
value.is_a?(String)
|
|
52
|
+
when :integer
|
|
53
|
+
value.is_a?(Integer)
|
|
54
|
+
when :float
|
|
55
|
+
value.is_a?(Float) || value.is_a?(Integer)
|
|
56
|
+
when :boolean
|
|
57
|
+
[true, false].include?(value)
|
|
58
|
+
when :uuid
|
|
59
|
+
value.is_a?(String) && uuid_format?(value)
|
|
60
|
+
when :money
|
|
61
|
+
value.is_a?(Numeric) && value >= 0
|
|
62
|
+
when :timestamp, :datetime
|
|
63
|
+
value.is_a?(Time) || value.is_a?(DateTime)
|
|
64
|
+
when :date
|
|
65
|
+
value.is_a?(Date)
|
|
66
|
+
when :enum
|
|
67
|
+
enum_values.include?(value)
|
|
68
|
+
when :hash
|
|
69
|
+
value.is_a?(Hash)
|
|
70
|
+
when :array
|
|
71
|
+
value.is_a?(Array)
|
|
72
|
+
else
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def validate_type!
|
|
80
|
+
unless VALID_TYPES.include?(type)
|
|
81
|
+
raise Error, "Invalid attribute type '#{type}'. Valid types: #{VALID_TYPES.join(', ')}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_options!
|
|
86
|
+
if type == :enum && enum_values.empty?
|
|
87
|
+
raise Error, "Enum attribute '#{name}' must specify :values option"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def uuid_format?(value)
|
|
92
|
+
value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class AutoMount
|
|
3
|
+
attr_reader :app, :config, :mounted_facets
|
|
4
|
+
|
|
5
|
+
def initialize(app = nil, config: {})
|
|
6
|
+
@app = app || detect_app
|
|
7
|
+
@config = Facera.configuration
|
|
8
|
+
@mounted_facets = {}
|
|
9
|
+
@logger = Logger.new($stdout)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mount!
|
|
13
|
+
log_header
|
|
14
|
+
discover_definitions
|
|
15
|
+
mount_facets
|
|
16
|
+
mount_introspection if @config.introspection
|
|
17
|
+
mount_dashboard if @config.dashboard
|
|
18
|
+
log_summary
|
|
19
|
+
@mounted_facets
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def log_header
|
|
25
|
+
@logger.info "\n" + "=" * 80
|
|
26
|
+
@logger.info "💎 Facera v#{Facera::VERSION} - Auto-Mounting"
|
|
27
|
+
@logger.info "=" * 80
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def discover_definitions
|
|
31
|
+
# Use Facera's loader to auto-discover cores, adapters, and facets
|
|
32
|
+
loader = Loader.new(logger: @logger)
|
|
33
|
+
loader.load_all!
|
|
34
|
+
|
|
35
|
+
@logger.info "\n📊 Found:"
|
|
36
|
+
@logger.info " Cores: #{Registry.cores.count}"
|
|
37
|
+
@logger.info " Adapters: #{AdapterRegistry.all.count}"
|
|
38
|
+
@logger.info " Facets: #{Registry.facets.count}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mount_facets
|
|
42
|
+
@logger.info "\n🚀 Mounting facets:"
|
|
43
|
+
|
|
44
|
+
Registry.facets.each do |name, facet|
|
|
45
|
+
next unless @config.facet_enabled?(name)
|
|
46
|
+
|
|
47
|
+
path = "#{@config.base_path}#{@config.path_for_facet(name)}"
|
|
48
|
+
api = Grape::APIGenerator.for_facet(name)
|
|
49
|
+
|
|
50
|
+
mount_api(api, path)
|
|
51
|
+
|
|
52
|
+
@mounted_facets[name] = {
|
|
53
|
+
path: path,
|
|
54
|
+
api: api,
|
|
55
|
+
endpoints: api.routes.count
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@logger.info " ✓ #{name.to_s.ljust(15)} → #{path.ljust(25)} (#{api.routes.count} endpoints)"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mount_introspection
|
|
63
|
+
path = "#{@config.base_path}/facera"
|
|
64
|
+
api = IntrospectionAPI
|
|
65
|
+
|
|
66
|
+
mount_api(api, path)
|
|
67
|
+
|
|
68
|
+
@logger.info "\n📚 Introspection API:"
|
|
69
|
+
@logger.info " ✓ Mounted at #{path}"
|
|
70
|
+
@logger.info " • #{path}/introspect - Full introspection"
|
|
71
|
+
@logger.info " • #{path}/cores - All cores"
|
|
72
|
+
@logger.info " • #{path}/facets - All facets"
|
|
73
|
+
@logger.info " • #{path}/openapi - OpenAPI specs"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def mount_api(api, path)
|
|
77
|
+
if defined?(Rails)
|
|
78
|
+
Rails.application.routes.draw do
|
|
79
|
+
mount api => path
|
|
80
|
+
end
|
|
81
|
+
elsif @app.respond_to?(:map)
|
|
82
|
+
# Rack app with map support
|
|
83
|
+
@app.map(path) { run api }
|
|
84
|
+
else
|
|
85
|
+
# Simple rack app
|
|
86
|
+
@app = Rack::URLMap.new(
|
|
87
|
+
path => api,
|
|
88
|
+
'/' => @app
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def mount_dashboard
|
|
94
|
+
# Dashboard will be implemented in a future phase
|
|
95
|
+
# For now, just log that it would be mounted
|
|
96
|
+
@logger.info "\n🎨 Dashboard: #{@config.base_path}/facera/ui (coming soon)"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def log_summary
|
|
100
|
+
@logger.info "\n" + "=" * 80
|
|
101
|
+
@logger.info "✨ Facera ready! #{@mounted_facets.count} facets mounted"
|
|
102
|
+
@logger.info "=" * 80 + "\n"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def detect_app
|
|
106
|
+
if defined?(Rails)
|
|
107
|
+
Rails.application
|
|
108
|
+
elsif defined?(Sinatra::Application)
|
|
109
|
+
Sinatra::Application
|
|
110
|
+
else
|
|
111
|
+
# Return a basic Rack builder
|
|
112
|
+
Rack::Builder.new
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class << self
|
|
118
|
+
def auto_mount!(app = nil, config: {})
|
|
119
|
+
AutoMount.new(app, config: config).mount!
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
require 'logger'
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class Capability
|
|
3
|
+
attr_reader :name, :type, :entity_name, :required_params, :optional_params,
|
|
4
|
+
:preconditions, :validations, :transitions, :field_setters, :action_name,
|
|
5
|
+
:execute_block
|
|
6
|
+
|
|
7
|
+
VALID_TYPES = [:create, :get, :update, :delete, :list, :action].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(name, type:)
|
|
10
|
+
@name = name.to_sym
|
|
11
|
+
@type = type.to_sym
|
|
12
|
+
@entity_name = nil
|
|
13
|
+
@required_params = []
|
|
14
|
+
@optional_params = []
|
|
15
|
+
@preconditions = []
|
|
16
|
+
@validations = []
|
|
17
|
+
@transitions = []
|
|
18
|
+
@field_setters = {}
|
|
19
|
+
@action_name = nil
|
|
20
|
+
@execute_block = nil
|
|
21
|
+
|
|
22
|
+
validate_type!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def entity(name)
|
|
26
|
+
@entity_name = name.to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def requires(*param_names)
|
|
30
|
+
param_names.each do |param_name|
|
|
31
|
+
@required_params << param_name.to_sym
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def optional(*param_names)
|
|
36
|
+
param_names.each do |param_name|
|
|
37
|
+
@optional_params << param_name.to_sym
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def precondition(&block)
|
|
42
|
+
@preconditions << block
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validates(&block)
|
|
46
|
+
@validations << block
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def transitions_to(state)
|
|
50
|
+
@transitions << state.to_sym
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sets(field_values)
|
|
54
|
+
@field_setters.merge!(field_values)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def action(name)
|
|
58
|
+
@action_name = name.to_sym
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def returns(type)
|
|
62
|
+
@return_type = type.to_sym
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def execute(&block)
|
|
66
|
+
@execute_block = block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def has_execute_block?
|
|
70
|
+
!@execute_block.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def filterable(*param_names)
|
|
74
|
+
@filterable_params ||= []
|
|
75
|
+
param_names.each do |param_name|
|
|
76
|
+
@filterable_params << param_name.to_sym
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def filterable_params
|
|
81
|
+
@filterable_params || []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_params(params)
|
|
85
|
+
errors = []
|
|
86
|
+
|
|
87
|
+
# Check required parameters
|
|
88
|
+
required_params.each do |param|
|
|
89
|
+
if params[param].nil? && params[param.to_s].nil?
|
|
90
|
+
errors << "#{param} is required"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
errors
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_preconditions(context)
|
|
98
|
+
preconditions.all? do |precondition|
|
|
99
|
+
context.instance_eval(&precondition)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_business_rules(context)
|
|
104
|
+
validations.all? do |validation|
|
|
105
|
+
context.instance_eval(&validation)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def validate_type!
|
|
112
|
+
unless VALID_TYPES.include?(type)
|
|
113
|
+
raise Error, "Invalid capability type '#{type}'. Valid types: #{VALID_TYPES.join(', ')}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class CapabilityAccess
|
|
3
|
+
attr_reader :allowed_capabilities, :denied_capabilities, :scopes
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@allowed_capabilities = :all
|
|
7
|
+
@denied_capabilities = []
|
|
8
|
+
@scopes = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def allow(*capability_names)
|
|
12
|
+
if capability_names.first == :all
|
|
13
|
+
@allowed_capabilities = :all
|
|
14
|
+
else
|
|
15
|
+
@allowed_capabilities = [] if @allowed_capabilities == :all
|
|
16
|
+
@allowed_capabilities.concat(capability_names.map(&:to_sym))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deny(*capability_names)
|
|
21
|
+
@denied_capabilities.concat(capability_names.map(&:to_sym))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def scope(capability_name, &block)
|
|
25
|
+
raise Error, "Scope for '#{capability_name}' must have a block" unless block_given?
|
|
26
|
+
@scopes[capability_name.to_sym] = block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def allowed?(capability_name)
|
|
30
|
+
cap_sym = capability_name.to_sym
|
|
31
|
+
|
|
32
|
+
# Check if explicitly denied
|
|
33
|
+
return false if @denied_capabilities.include?(cap_sym)
|
|
34
|
+
|
|
35
|
+
# Check if in allowed list
|
|
36
|
+
if @allowed_capabilities == :all
|
|
37
|
+
true
|
|
38
|
+
else
|
|
39
|
+
@allowed_capabilities.include?(cap_sym)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def scope_for(capability_name)
|
|
44
|
+
@scopes[capability_name.to_sym]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def has_scope?(capability_name)
|
|
48
|
+
@scopes.key?(capability_name.to_sym)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def allowed_capability_names(core)
|
|
52
|
+
if @allowed_capabilities == :all
|
|
53
|
+
core.capabilities.keys - @denied_capabilities
|
|
54
|
+
else
|
|
55
|
+
@allowed_capabilities - @denied_capabilities
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :base_path, :version, :dashboard, :generate_docs, :introspection
|
|
4
|
+
attr_reader :facet_paths, :disabled_facets, :authentication_handlers, :middleware_handlers
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@base_path = '/api'
|
|
8
|
+
@version = 'v1'
|
|
9
|
+
@dashboard = true
|
|
10
|
+
@generate_docs = true
|
|
11
|
+
@introspection = true
|
|
12
|
+
@facet_paths = {}
|
|
13
|
+
@disabled_facets = []
|
|
14
|
+
@authentication_handlers = {}
|
|
15
|
+
@middleware_handlers = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def facet_path(facet_name, path)
|
|
19
|
+
@facet_paths[facet_name.to_sym] = path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def disable_facet(facet_name)
|
|
23
|
+
@disabled_facets << facet_name.to_sym
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def authenticate(facet_name, &block)
|
|
27
|
+
raise Error, "Authentication block required for facet '#{facet_name}'" unless block_given?
|
|
28
|
+
@authentication_handlers[facet_name.to_sym] = block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def middleware_for(facet_name, &block)
|
|
32
|
+
raise Error, "Middleware block required for facet '#{facet_name}'" unless block_given?
|
|
33
|
+
@middleware_handlers[facet_name.to_sym] = block
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def path_for_facet(facet_name)
|
|
37
|
+
@facet_paths[facet_name.to_sym] || default_path_for(facet_name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def facet_enabled?(facet_name)
|
|
41
|
+
!@disabled_facets.include?(facet_name.to_sym)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authentication_handler_for(facet_name)
|
|
45
|
+
@authentication_handlers[facet_name.to_sym]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def middleware_handler_for(facet_name)
|
|
49
|
+
@middleware_handlers[facet_name.to_sym]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def default_path_for(facet_name)
|
|
55
|
+
case facet_name.to_sym
|
|
56
|
+
when :external
|
|
57
|
+
"/#{@version}"
|
|
58
|
+
when :internal
|
|
59
|
+
"/internal/#{@version}"
|
|
60
|
+
when :operator
|
|
61
|
+
"/operator/#{@version}"
|
|
62
|
+
when :agent
|
|
63
|
+
"/agent/#{@version}"
|
|
64
|
+
else
|
|
65
|
+
"/#{facet_name}/#{@version}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
def configuration
|
|
72
|
+
@configuration ||= Configuration.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def configure
|
|
76
|
+
yield(configuration) if block_given?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reset_configuration!
|
|
80
|
+
@configuration = Configuration.new
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Facera
|
|
2
|
+
# Lightweight context object for block evaluation
|
|
3
|
+
# Replaces OpenStruct with a simpler, faster implementation
|
|
4
|
+
class Context
|
|
5
|
+
def initialize(data = {})
|
|
6
|
+
@data = data.transform_keys(&:to_sym)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def method_missing(method, *args)
|
|
10
|
+
if args.empty?
|
|
11
|
+
# Getter
|
|
12
|
+
@data[method]
|
|
13
|
+
elsif method.to_s.end_with?('=')
|
|
14
|
+
# Setter
|
|
15
|
+
@data[method.to_s.chomp('=').to_sym] = args.first
|
|
16
|
+
else
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def respond_to_missing?(method, include_private = false)
|
|
22
|
+
@data.key?(method) || method.to_s.end_with?('=') || super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
@data
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|