- 
                Notifications
    
You must be signed in to change notification settings  - Fork 918
 
GODRIVER-3605 Refactor StringN #2128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
          API Change Report./v2/x/bsonx/bsoncoreincompatible changesArray.StringN: changed from func(int) string to func(int) (string, bool)  | 
    
f7afe10    to
    050c014      
    Compare
  
    f9a5d13    to
    4d17e8b      
    Compare
  
    d2d4214    to
    0563a2b      
    Compare
  
    | str, truncated := bsoncore.Document(msg).StringN(int(width)) | ||
| 
               | 
          ||
| // If the last byte is not a closing bracket, then the document was truncated | ||
| if len(str) > 0 && str[len(str)-1] != '}' { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line causes GODRIVER-3561
acac4b7    to
    620794a      
    Compare
  
    620794a    to
    3d68a75      
    Compare
  
    There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work, Qingyang. Thank you for taking this on 👍
        
          
                x/bsonx/bsoncore/document.go
              
                Outdated
          
        
      | } | ||
| 
               | 
          ||
| var buf strings.Builder | ||
| // stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?
| func (d Document) StringN(n int) string { | ||
| if len(d) < 5 || n <= 0 { | ||
| return "" | ||
| func (d Document) StringN(n int) (string, bool) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use named return values.
E.g.
func (d Document) StringN(n int) (val string, truncated bool)Edit: Note that named return values are only generally a good idea on short functions (<50 lines). Disregard this suggestion if StringN becomes longer than 50 lines.
        
          
                x/bsonx/bsoncore/document.go
              
                Outdated
          
        
      | if n <= 0 { | ||
| if l, _, ok := ReadLength(d); !ok || l < 5 { | ||
| return "", false | ||
| } | ||
| return "", true | ||
| } | ||
| return d.stringN(n) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's interesting that calling StringN(0) will return an empty string but stringN(0) will return the entire document. The asymmetry makes the code fairly difficult to read. Should we update StringN so that an N <= 0 argument returns the full document?
 func (d Document) String(n int) (string, bool) {
	if n <= 0 {
		return d.stringN(0)
	}
	return d.stringN(n)
}Ultimately, it's unclear to me why one would call StringN(0) expecting an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By using stringN(), we can keep the original behavior of StringN(n), which is to return an empty string when n <= 0, while also allowing it to be reused by String(). However, I will be happy to merge stringN() and StringN() if we all accept returning the entire document on StringN(0).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that makes sense. Will let @matthewdale opine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a slight preference for -1 (i.e. all negative) vs 0 (i.e. all non-positive) because it more closely matches the semantics of string truncation (i.e. StringN(0) == String()[:0]), although there's not a clear use case for passing either -1 or 0.
Is there a reason we want to change the behavior from the original where we pass math.IntMax to get the full document?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 (i.e. all negative) vs 0 (i.e. all non-positive) seems a more sensible interface.
It's just for a better compatibility that not using a particular value like math.IntMax to get the entire document.
        
          
                x/bsonx/bsoncore/document.go
              
                Outdated
          
        
      | if !ok || length < 5 { | ||
| return "", false | ||
| } | ||
| length -= (4 /* length bytes */ + 1 /* final null byte */) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Can we re-write this to use // ?
length := (4 + // length bytes
	1) // final null bytes| truncatedStr := bsoncoreutil.Truncate(str, n-buf.Len()) | ||
| buf.WriteString(truncatedStr) | ||
| 
               | 
          ||
| for length > 0 && !truncated { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] The loop logic is hard to follow, suggest adding comments. I think the following are sufficient:
- determine remaining budget 
l := n - buf.Len() - If 
buf.Len() >= n, then mark truncated and break - if 
!first, then write comma, then decrementland possibly break - ReadElement, subtract its full length, exit on 
!ok - Delegate to 
elem.stringN(l), write its str, record its truncated flag 
| l = n - buf.Len() | ||
| } | ||
| if !first { | ||
| buf.WriteByte(',') | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure we should be writing the comma here? Without testing directly it seems unintuitive. What if n=1 or something and there isn't enough room for a comma?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another issue is that we don't need a comma if the document only contains one element, even if there is enough space.
        
          
                x/bsonx/bsoncore/document_test.go
              
                Outdated
          
        
      | for _, tc := range testCases { | ||
| for n := -1; n <= len(tc.want)+1; n++ { | ||
| t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { | ||
| got, _ := tc.doc.StringN(n) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should validate the expected values for the the truncation bool.
        
          
                x/bsonx/bsoncore/element.go
              
                Outdated
          
        
      | func (e Element) StringN(n int) string { | ||
| func (e Element) StringN(n int) (string, bool) { | ||
| if n <= 0 { | ||
| return "", len(e) > 0 | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0 as "no limit", see comment on document.StringN().
        
          
                x/bsonx/bsoncore/value.go
              
                Outdated
          
        
      | str, truncated := v.stringN(n) | ||
| if n <= 0 { | ||
| return "" | ||
| return "", len(str) > 0 | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0 as "no limit", see comment on document.StringN().
        
          
                x/bsonx/bsoncore/array.go
              
                Outdated
          
        
      | if lens, _, _ := ReadLength(a); lens < 5 || n <= 0 { | ||
| return "" | ||
| func (a Array) StringN(n int) (string, bool) { | ||
| if n <= 0 { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider treating n<=0 as "no limit", see comment on document.StringN().
        
          
                x/bsonx/bsoncore/array_test.go
              
                Outdated
          
        
      | 
               | 
          ||
| for _, tc := range testCases { | ||
| for n := -1; n <= len(tc.want)+1; n++ { | ||
| t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestArray_String and TestArray_StringN and moving the test cases to a package-level variable (e.g. var arrayStringTestCases = ...).
        
          
                x/bsonx/bsoncore/document_test.go
              
                Outdated
          
        
      | 
               | 
          ||
| for _, tc := range testCases { | ||
| for n := -1; n <= len(tc.want)+1; n++ { | ||
| t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestDocument_String and TestDocument_StringN and moving the test cases to a package-level variable (e.g. var documentStringTestCases = ...).
        
          
                x/bsonx/bsoncore/value_test.go
              
                Outdated
          
        
      | t.Run(tc.description, func(t *testing.T) { | ||
| got := tc.val.StringN(tc.n) | ||
| for n := -1; n <= len(tc.want)+1; n++ { | ||
| t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring these into two test functions TestValue_String and TestValue_StringN and moving the test cases to a package-level variable (e.g. var valueStringTestCases = ...).
        
          
                x/bsonx/bsoncore/value_test.go
              
                Outdated
          
        
      | {15, `"𨉟呐㗂越"`}, | ||
| {21, `"𨉟呐㗂越"`}, | ||
| } { | ||
| t.Run(fmt.Sprintf("StringN multi-byte string n==%d", tc.n), func(t *testing.T) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Consider factoring this into a separate test function TestArray_StringN_Multibyte.
| } | ||
| 
               | 
          ||
| return buf.String() | ||
| return buf.String(), truncated | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to truncate the string in the buffer?
E.g.
bsoncoreutil.Truncate(buf.String())There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not technically necessary, as the buffer has been truncated previously. However, an extra-truncating sounds like a good idea for an additional layer of security
| } | ||
| 
               | 
          ||
| return buf.String() | ||
| return buf.String(), truncated | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to truncate the string in the buffer?
E.g.
bsoncoreutil.Truncate(buf.String())There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not technically necessary, as the buffer has been truncated previously. However, an extra-truncating sounds like a good idea for an additional layer of security
74d2883    to
    555951f      
    Compare
  
    There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
This reverts commit a709a1d.
GODRIVER-3605
GODRIVER-3561
Summary
Refactor
StringNBackground & Motivation
Originally,
Element.StringN()does not truncate strings as expected.This PR also adds a return flag to
StringN()to indicate whether the string has been truncated.