Passed
Push — master ( 3ad63e...68840f )
by Tolga
01:29
created

coverage.calculateTotalCoverage   B

Complexity

Conditions 6

Size

Total Lines 36
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 29
nop 1
dl 0
loc 36
rs 8.2506
c 0
b 0
f 0
1
package coverage // Coverage analysis package
2
import (         // Package imports
3
	"fmt"    // Formatting
4
	"slices" // Slice operations
5
6
	"github.com/Permify/permify/pkg/attribute"
7
	"github.com/Permify/permify/pkg/development/file"
8
	"github.com/Permify/permify/pkg/dsl/compiler"
9
	"github.com/Permify/permify/pkg/dsl/parser"
10
	base "github.com/Permify/permify/pkg/pb/base/v1"
11
	"github.com/Permify/permify/pkg/tuple"
12
)
13
14
// SchemaCoverageInfo - Schema coverage info
15
type SchemaCoverageInfo struct {
16
	EntityCoverageInfo         []EntityCoverageInfo // Entity coverage details
17
	TotalRelationshipsCoverage int                  // Total relationships coverage
18
	TotalAttributesCoverage    int                  // Total attributes coverage
19
	TotalAssertionsCoverage    int                  // Total assertions coverage
20
} // End SchemaCoverageInfo
21
22
// EntityCoverageInfo - Entity coverage info
23
type EntityCoverageInfo struct {
24
	EntityName string
25
26
	UncoveredRelationships       []string
27
	CoverageRelationshipsPercent int
28
29
	UncoveredAttributes       []string
30
	CoverageAttributesPercent int
31
32
	UncoveredAssertions       map[string][]string
33
	CoverageAssertionsPercent map[string]int
34
}
35
36
// SchemaCoverage
37
//
38
// schema:
39
//
40
//	entity user {}
41
//
42
//	entity organization {
43
//	    // organizational roles
44
//	    relation admin @user
45
//	    relation member @user
46
//	}
47
//
48
//	entity repository {
49
//	    // represents repositories parent organization
50
//	    relation parent @organization
51
//
52
//	    // represents owner of this repository
53
//	    relation owner  @user @organization#admin
54
//
55
//	    // permissions
56
//	    permission edit   = parent.admin or owner
57
//	    permission delete = owner
58
//	}
59
//
60
// - relationships coverage
61
//
62
// organization#admin@user
63
// organization#member@user
64
// repository#parent@organization
65
// repository#owner@user
66
// repository#owner@organization#admin
67
//
68
// - assertions coverage
69
//
70
// repository#edit
71
// repository#delete
72
type SchemaCoverage struct {
73
	EntityName    string
74
	Relationships []string
75
	Attributes    []string
76
	Assertions    []string
77
}
78
79
func Run(shape file.Shape) SchemaCoverageInfo {
80
	p, err := parser.NewParser(shape.Schema).Parse()
81
	if err != nil {
82
		return SchemaCoverageInfo{}
83
	}
84
85
	definitions, _, err := compiler.NewCompiler(true, p).Compile()
86
	if err != nil {
87
		return SchemaCoverageInfo{}
88
	}
89
90
	schemaCoverageInfo := SchemaCoverageInfo{}
91
92
	refs := make([]SchemaCoverage, len(definitions))
93
	for idx, entityDef := range definitions { // Build entity references
94
		refs[idx] = references(entityDef) // Extract references
95
	} // References built
96
97
	// Iterate through the schema coverage references
98
	for _, ref := range refs {
99
		// Initialize EntityCoverageInfo for the current entity
100
		entityCoverageInfo := EntityCoverageInfo{
101
			EntityName:                   ref.EntityName,
102
			UncoveredRelationships:       []string{},
103
			UncoveredAttributes:          []string{},
104
			CoverageAssertionsPercent:    map[string]int{},
105
			UncoveredAssertions:          map[string][]string{},
106
			CoverageRelationshipsPercent: 0,
107
			CoverageAttributesPercent:    0,
108
		}
109
110
		// Calculate relationships coverage
111
		er := relationships(ref.EntityName, shape.Relationships)
112
113
		for _, relationship := range ref.Relationships {
114
			if !slices.Contains(er, relationship) {
115
				entityCoverageInfo.UncoveredRelationships = append(entityCoverageInfo.UncoveredRelationships, relationship)
116
			}
117
		}
118
119
		entityCoverageInfo.CoverageRelationshipsPercent = calculateCoveragePercent(
120
			ref.Relationships,
121
			entityCoverageInfo.UncoveredRelationships,
122
		)
123
124
		// Calculate attributes coverage
125
		at := attributes(ref.EntityName, shape.Attributes)
126
127
		for _, attr := range ref.Attributes {
128
			if !slices.Contains(at, attr) {
129
				entityCoverageInfo.UncoveredAttributes = append(entityCoverageInfo.UncoveredAttributes, attr)
130
			}
131
		}
132
133
		entityCoverageInfo.CoverageAttributesPercent = calculateCoveragePercent(
134
			ref.Attributes,
135
			entityCoverageInfo.UncoveredAttributes,
136
		)
137
138
		// Calculate assertions coverage for each scenario
139
		for _, s := range shape.Scenarios {
140
			ca := assertions(ref.EntityName, s.Checks, s.EntityFilters)
141
142
			for _, assertion := range ref.Assertions {
143
				if !slices.Contains(ca, assertion) {
144
					entityCoverageInfo.UncoveredAssertions[s.Name] = append(entityCoverageInfo.UncoveredAssertions[s.Name], assertion)
145
				}
146
			}
147
148
			entityCoverageInfo.CoverageAssertionsPercent[s.Name] = calculateCoveragePercent(
149
				ref.Assertions,
150
				entityCoverageInfo.UncoveredAssertions[s.Name],
151
			)
152
		}
153
154
		schemaCoverageInfo.EntityCoverageInfo = append(schemaCoverageInfo.EntityCoverageInfo, entityCoverageInfo)
155
	}
156
157
	// Calculate total coverage for relationships, attributes and assertions
158
	relationshipsCoverage, attributesCoverage, assertionsCoverage := calculateTotalCoverage(schemaCoverageInfo.EntityCoverageInfo) // Calculate totals
159
	schemaCoverageInfo.TotalRelationshipsCoverage = relationshipsCoverage                                                          // Set total relationships
160
	schemaCoverageInfo.TotalAttributesCoverage = attributesCoverage                                                                // Set total attributes
161
	schemaCoverageInfo.TotalAssertionsCoverage = assertionsCoverage                                                                // Set total assertions
162
	return schemaCoverageInfo                                                                                                      // Return coverage info
163
}
164
165
// calculateCoveragePercent - Calculate coverage percentage based on total and uncovered elements
166
func calculateCoveragePercent(totalElements, uncoveredElements []string) int {
167
	coveragePercent := 100
168
	totalCount := len(totalElements)
169
170
	if totalCount != 0 {
171
		coveredCount := totalCount - len(uncoveredElements)
172
		coveragePercent = (coveredCount * 100) / totalCount
173
	}
174
175
	return coveragePercent
176
}
177
178
// calculateTotalCoverage - Calculate total relationships and assertions coverage
179
func calculateTotalCoverage(entities []EntityCoverageInfo) (int, int, int) {
180
	totalRelationships := 0        // Total relationships counter
181
	totalCoveredRelationships := 0 // Covered relationships counter
182
	totalAttributes := 0           // Total attributes counter
183
	totalCoveredAttributes := 0    // Covered attributes counter
184
	totalAssertions := 0           // Total assertions counter
185
	totalCoveredAssertions := 0    // Covered assertions counter
186
	// Process all entities to calculate coverage
187
	for _, entity := range entities { // Process each entity
188
		totalRelationships++                                                // Count relationships
189
		totalCoveredRelationships += entity.CoverageRelationshipsPercent    // Add covered
190
		totalAttributes++                                                   // Count attributes
191
		totalCoveredAttributes += entity.CoverageAttributesPercent          // Add covered attributes
192
		for _, assertionPercent := range entity.CoverageAssertionsPercent { // Process assertions
193
			totalAssertions++                          // Increment assertion count
194
			totalCoveredAssertions += assertionPercent // Add covered assertion
195
		} // Assertions processed
196
	} // Entities processed
197
	// Calculate average coverage percentages for all entities (guard zero denominators)
198
	var totalRelationshipsCoverage, totalAttributesCoverage, totalAssertionsCoverage int
199
	if totalRelationships > 0 {
200
		totalRelationshipsCoverage = totalCoveredRelationships / totalRelationships
201
	} else {
202
		totalRelationshipsCoverage = 100
203
	}
204
	if totalAttributes > 0 {
205
		totalAttributesCoverage = totalCoveredAttributes / totalAttributes
206
	} else {
207
		totalAttributesCoverage = 100
208
	}
209
	if totalAssertions > 0 {
210
		totalAssertionsCoverage = totalCoveredAssertions / totalAssertions
211
	} else {
212
		totalAssertionsCoverage = 100
213
	}
214
	return totalRelationshipsCoverage, totalAttributesCoverage, totalAssertionsCoverage // Return totals
215
} // End calculateTotalCoverage
216
// References - Get references for a given entity
217
func references(entity *base.EntityDefinition) (coverage SchemaCoverage) {
218
	// Set the entity name in the coverage struct
219
	coverage.EntityName = entity.GetName()
220
	// Iterate over all relations in the entity
221
	for _, relation := range entity.GetRelations() {
222
		// Iterate over all references within each relation
223
		for _, reference := range relation.GetRelationReferences() {
224
			if reference.GetRelation() != "" {
225
				// Format and append the relationship to the coverage struct
226
				formattedRelationship := fmt.Sprintf("%s#%s@%s#%s", entity.GetName(), relation.GetName(), reference.GetType(), reference.GetRelation())
227
				coverage.Relationships = append(coverage.Relationships, formattedRelationship)
228
			} else {
229
				formattedRelationship := fmt.Sprintf("%s#%s@%s", entity.GetName(), relation.GetName(), reference.GetType())
230
				coverage.Relationships = append(coverage.Relationships, formattedRelationship)
231
			}
232
		}
233
	}
234
	// Iterate over all attributes in the entity
235
	for _, attr := range entity.GetAttributes() {
236
		// Format and append the attribute to the coverage struct
237
		formattedAttribute := fmt.Sprintf("%s#%s", entity.GetName(), attr.GetName())
238
		coverage.Attributes = append(coverage.Attributes, formattedAttribute)
239
	}
240
	// Iterate over all permissions in the entity
241
	for _, permission := range entity.GetPermissions() {
242
		// Format and append the permission to the coverage struct
243
		formattedPermission := fmt.Sprintf("%s#%s", entity.GetName(), permission.GetName())
244
		coverage.Assertions = append(coverage.Assertions, formattedPermission)
245
	}
246
	// Return the coverage struct
247
	return
248
}
249
250
// relationships - Get relationships for a given entity
251
func relationships(en string, relationships []string) []string {
252
	var rels []string
253
	for _, relationship := range relationships {
254
		tup, err := tuple.Tuple(relationship)
255
		if err != nil {
256
			return []string{}
257
		}
258
		if tup.GetEntity().GetType() != en {
259
			continue
260
		}
261
		// Check if the reference has a relation name
262
		if tup.GetSubject().GetRelation() != "" {
263
			// Format and append the relationship to the coverage struct
264
			rels = append(rels, fmt.Sprintf("%s#%s@%s#%s", tup.GetEntity().GetType(), tup.GetRelation(), tup.GetSubject().GetType(), tup.GetSubject().GetRelation()))
265
		} else {
266
			rels = append(rels, fmt.Sprintf("%s#%s@%s", tup.GetEntity().GetType(), tup.GetRelation(), tup.GetSubject().GetType()))
267
		}
268
		// Format ad append the relationship without the relation name to the coverage struct
269
	}
270
	return rels
271
}
272
273
// attributes - Get attributes for a given entity
274
func attributes(en string, attributes []string) []string {
275
	attrs := make([]string, len(attributes))
276
	for index, attrStr := range attributes { // Iterate attribute strings
277
		a, err := attribute.Attribute(attrStr)
278
		if err != nil {
279
			return []string{}
280
		}
281
		if a.GetEntity().GetType() != en {
282
			continue
283
		}
284
		attrs[index] = fmt.Sprintf("%s#%s", a.GetEntity().GetType(), a.GetAttribute()) // Format attribute
285
	} // End iteration
286
	return attrs // Return attributes
287
} // End attributes
288
289
// assertions - Get assertions for a given entity
290
func assertions(en string, checks []file.Check, filters []file.EntityFilter) []string {
291
	// Initialize an empty slice to store the resulting assertions
292
	var asrts []string
293
294
	// Iterate over each check in the checks slice
295
	for _, assertion := range checks {
296
		// Get the corresponding entity object for the current assertion
297
		ca, err := tuple.E(assertion.Entity)
298
		if err != nil {
299
			// If there's an error, return an empty slice
300
			return []string{}
301
		}
302
303
		// If the current entity type doesn't match the given entity type, continue to the next check
304
		if ca.GetType() != en {
305
			continue
306
		}
307
308
		// Iterate over the keys (permissions) in the Assertions map
309
		for permission := range assertion.Assertions {
310
			// Append the formatted permission string to the asrts slice
311
			asrts = append(asrts, fmt.Sprintf("%s#%s", ca.GetType(), permission))
312
		}
313
	}
314
315
	// Iterate over each entity filter in the filters slice
316
	for _, assertion := range filters {
317
		// If the current entity type doesn't match the given entity type, continue to the next filter
318
		if assertion.EntityType != en {
319
			continue
320
		}
321
322
		// Iterate over the keys (permissions) in the Assertions map
323
		for permission := range assertion.Assertions {
324
			// Append the formatted permission string to the asrts slice
325
			asrts = append(asrts, fmt.Sprintf("%s#%s", assertion.EntityType, permission))
326
		}
327
	}
328
329
	// Return the asrts slice containing the collected assertions
330
	return asrts
331
}
332