Attila Kovacs 629 Posted October 26, 2020 (edited) I was tired writing IncludeTrailingURLDelimiter(IncludeTrailingURLDelimiter(dir1)+dir2) ... etc. so I thought I can write a little URL Builder. As I'm a lazy dog, I did not want to go with classes, I wanted something self managed, but to have that I had to solve the auto initialization, so here I'm (see the code) Sure this is not the most resource saver solution, but this wasn't my goal. Any thoughts? Positive, negative, everything welcome, just that we can learn something. lURL := TURLBuilder.AddDir('dir').AddDir('dir2').AddFile('file'); unit URLBuilder; interface type TURLBuilder = class //record private type TURLBuilder = record private FUrl: string; public function AddDir(const ADir: string): TURLBuilder.TURLBuilder; function AddFile(const AFile: string): TURLBuilder.TURLBuilder; class operator Implicit(a: TURLBuilder.TURLBuilder): string; class operator Explicit(a: TURLBuilder.TURLBuilder): string; end; public class function AddDir(const ADir: string): TURLBuilder.TURLBuilder; static; end; function IncludeTrailingURLDelimiter(const AURL: string): string; implementation function IncludeTrailingURLDelimiter(const AURL: string): string; begin Result := AURL; if (AURL <> '') and (AURL[High(AURL)] <> '/') then Result := Result + '/'; end; { TURLBuilder } class function TURLBuilder.AddDir(const ADir: string): TURLBuilder.TURLBuilder; begin Result.FUrl := IncludeTrailingURLDelimiter(ADir); end; { TURLBuilder.TURLBuilder } function TURLBuilder.TURLBuilder.AddDir(const ADir: string): TURLBuilder.TURLBuilder; begin Result.FUrl := FUrl + IncludeTrailingURLDelimiter(ADir); end; function TURLBuilder.TURLBuilder.AddFile(const AFile: string): TURLBuilder.TURLBuilder; begin Result.FUrl := FUrl + AFile; end; class operator TURLBuilder.TURLBuilder.Explicit(a: TURLBuilder.TURLBuilder): string; begin Result := a.FUrl; end; class operator TURLBuilder.TURLBuilder.Implicit(a: TURLBuilder.TURLBuilder): string; begin Result := a.FUrl; end; end. Edited October 26, 2020 by Attila Kovacs Share this post Link to post
Mahdi Safsafi 225 Posted October 26, 2020 What about something like that ? type TURL = type string; TURLHelper = record helper for TURL function AddFile(const AFile: string): TURL; function AddDir(const ADir: string): TURL; end; Share this post Link to post
Kryvich 165 Posted October 26, 2020 First, I must note that IncludeTrailingURLDelimiter(IncludeTrailingURLDelimiter(dir1)+dir2) equals to IncludeTrailingURLDelimiter(dir1)+IncludeTrailingURLDelimiter(dir2) If your problems are 1) the function name is too long, and 2) you prefer the dot notation, then type TURL = type string; TURLHelper = record helper for TURL function AddDelimiter: TURL; end; function TURLHelper.AddDelimiter: TURL; begin Result := IncludeTrailingURLDelimiter(Self); end; and use it as follows: myURI := Dir1.AddDelimiter + Dir2.AddDelimiter + FileName; Share this post Link to post
Attila Kovacs 629 Posted October 26, 2020 @Kryvich Yes, but It's slightly different from my wishes. Dir1 Dir2 has to be TURL. Share this post Link to post
Kryvich 165 Posted October 26, 2020 There are two points. 1. It would be possible to add a helper for the string type, but then the standard helper for this type will stop working. 2. It is considered good style to have specialized string or numeric types for special uses. Share this post Link to post
Attila Kovacs 629 Posted October 26, 2020 @Kryvich I'll think about #2, sounds legit. thx Share this post Link to post
FredS 138 Posted October 26, 2020 Since we already have System.Net.URLClient.TURI I might simply add a record helper with a class function Combine. AUrl := TUri.Combine(Path1, Path2); Share this post Link to post
Marat1961 17 Posted October 27, 2020 (edited) // Conversion functions (return an empty path on error) TxPath = record const SEPARATOR = {$IFDEF MSWINDOWS} '/'; {$ELSE} '\'; {$ENDIF} var raw: string; public constructor From(const raw: string); class function CurrentDirectory: TxPath; static; procedure Clear; function Equals(const other: TxPath): Boolean; function IsEmpty: Boolean; function IsAbsolute: Boolean; function HasExtension(const ext: string): Boolean; function FileName: string; function FileStem: string; function Extension: string; function WithExtension(const ext: string): TxPath; function Parent: TxPath; function Join(const component: string): TxPath; overload; function Join(other: TxPath): TxPath; overload; function Expand(fromCurrentDirectory: Boolean = False): TxPath; function RelativeTo(base: TxPath): TxPath; // Conversions to and from platform independent representation (usually Unix) class function FromPortable(const repr: string): TxPath; static; function ToPortable: string; end; In my opinion, at this stage it is to reinvent the wheel again. Although sometimes there is a desire for it to be a separate entity. There is a System.IOUtils module that provides the required set of functionality. Edited October 27, 2020 by Marat1961 Share this post Link to post
Guest Posted October 27, 2020 How about a little tweaked version of Mahdi helper version type TURL = type string; TURLHelper = record helper for TURL function AddFile(const AFile: string): TURL; function AddDir(const ADir: string): TURL; function ToString: string; // or AsString end; var // use threadvar to make it multithread safe URL: TURL; // Global var, not thread safe when declared as var, for multithread safety declare and use local var, or it can be declared globally with threadvar procedure TForm6.Button1Click(Sender: TObject); var st: string; begin st := URL.AddDir('Base').AddDir('Sub').AddFile('BinaryFile').ToString; Memo1.Lines.Add(st); Memo1.Lines.Add(URL.AddDir('Base2').AddDir('Sub2').AddFile('BinaryFile2').ToString); end; { TURLHelper } function TURLHelper.AddDir(const ADir: string): TURL; begin Result := Self + '{dir}' + ADir + '/'; Self := Result; end; function TURLHelper.AddFile(const AFile: string): TURL; begin Result := Self + '{file}' + AFile; Self := Result; end; function TURLHelper.ToString: string; begin Result := Self; Self := ''; // Clear Self as it supposed to be used as global var, and should have produce string one time only end; And the result is {dir}Base/{dir}Sub/{file}BinaryFile {dir}Base/{dir}Base2/{dir}Sub2/{file}BinaryFile2 Share this post Link to post
Marat1961 17 Posted October 27, 2020 (edited) There must be a vision, ideas why this project is needed? Do you need to define the goals of the project and understand whether it is worth initiating such a project? Otherwise, an error may occur when starting the project. Wasted time on questionable jobs. I see the point of doing something if there is support: - different platforms (window, unix, ...); - rigid data typing (is it easy to confuse this type with any other string); - device concepts; - relative path; - absolute path (starting from the root directory); - transformation of presentation from one platform to another; - converting a relative path to an absolute one and vice versa. Edited October 27, 2020 by Marat1961 Share this post Link to post
Attila Kovacs 629 Posted October 27, 2020 @Kas Ob. I went through all these versions in my mind, even with clearing in the class operators, but it's just not right. You also have to know the implementation details for that version. @Marat1961 There is no desire on reinventing the wheel, I would have been pleased if there were something in the RTL. The cited func works with path separators and not url separators, as far as I can see. However the suggestion by @Kryvich to introduce a TURIPart=type string; and add some helpers sounds good. Share this post Link to post
Fr0sT.Brutal 900 Posted October 27, 2020 My variant: Url := TPath.CreateX('example.com') / 'sub' / 'sub1' / 'onemoresub' / 'index.html'; 'X' means 'Posix' path delimiters. Initially I used only platform-defined one but I needed both simultaneously (Posix-style for network addresses and Windows-style for local paths) Share this post Link to post
Guest Posted October 27, 2020 I read fun in the title so went after the most exotic variation, but will this do? function BuildURL(const aFileName: string; const aDirs: array of string): string; var I: Integer; begin Result := ''; for I := 0 to Length(aDirs) - 1 do Result := Result + '{dir}' + aDirs[I] + '/'; Result := Result + '{file}' + aFileName; end; Memo1.Lines.Add(BuildURL('F1.text', ['BaseDir', 'Sub1', 'Sub2'])); Memo1.Lines.Add(BuildURL('F2.text', [])); It is small and focused. Share this post Link to post
Marat1961 17 Posted October 27, 2020 (edited) https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/URI_syntax_diagram.svg/1602px-URI_syntax_diagram.svg.png Edited October 27, 2020 by Marat1961 Share this post Link to post
Mahdi Safsafi 225 Posted October 27, 2020 50 minutes ago, Attila Kovacs said: @Kas Ob. I went through all these versions in my mind, even with clearing in the class operators, but it's just not right. You also have to know the implementation details for that version. This doesn't sound problematic for me. A specialized string is expected to follow string rules hence same implementation details. After all that's what it designed for. Share this post Link to post
Guest Posted October 27, 2020 @Marat1961 The point from the OP question is not about building correct and valid URL per se, but what approach or tricks can be used to make the code short and reusable, as the title said having fun, so please join if you got short or different method, it doesn't require to be the best, it is food for thoughts thing, as one might find it inspiring else where in his code. Share this post Link to post
Marat1961 17 Posted October 27, 2020 (edited) From a usability point of view, it is desirable to store the path state inside a record. This is better than the helper in that we have a type with its own internal state. I am not a fan of using globals. The structure should look like a builder. It would be useful to add the opposite operation, parsing. Edited October 27, 2020 by Marat1961 Share this post Link to post
mvanrijnen 123 Posted October 27, 2020 (edited) Something like: tmp := THSUri.HTTPS .UserName('yourname') .Password('andpassword') .Host('www.website.com') .Port(1234) .Path('chapter') .Path('subchapter') .Path('subsubchaper') .Parameter('showall') .Parameter('yearstart', '1990') .Fragment('startreadinghere'); tmp.AsString Reults in: 'https://yourname:andpassword@www.website.com:1234/chapter/subchapter/subsubchaper?showall&yearstart=1990#startreadinghere' I'm using the scheme (HTTP, HTTPS) as the record constructor/initializer. .... public class function Scheme(const AScheme : THSURIScheme) : THSUri; static; class function HTTP : THSURI; static; class function HTTPS : THSURI; static; ..... implementation class function THSURI.Scheme(const AScheme: THSURIScheme): THSUri; begin // some needed and needless inits. Result.FScheme := AScheme; Result.FPort := 0; Result.FHost := string.Empty; Result.FUserName := string.Empty; Result.FPassword := string.Empty; Result.FPath := string.Empty; Result.FQuery := string.Empty; Result.FFragment := string.Empty; end; class function THSURI.HTTP: THSURI; begin Result := THSURI.Scheme(usHTTP); end; class function THSURI.HTTPS: THSURI; begin Result := THSURI.Scheme(usHTTPS); end; Edited October 27, 2020 by mvanrijnen 1 Share this post Link to post
Stefan Glienke 2002 Posted October 27, 2020 Fluent API with records containing managed fields (such as strings) unfortunately produces terrible code because the compiler produces an implicit variable for each method call. 1 1 Share this post Link to post
Marat1961 17 Posted October 27, 2020 2 minutes ago, Stefan Glienke said: Fluent API with records containing managed fields (such as strings) unfortunately produces terrible code because the compiler produces an implicit variable for each method call. Undoubtedly a string builder must be used inside, but what a convenient and intuitive interface! Share this post Link to post
Stefan Glienke 2002 Posted October 27, 2020 1 minute ago, Marat1961 said: Undoubtedly a string builder must be used inside, A string builder must be created and more importantly destroyed. Unless you are making this a custom managed record (which would be equally terrible for fluent API I guess - not tested yet) you would then need to explicitly call some Release/Free/whatever method on that record. If you were to make a fluent API on a record storing all the information those API methods must work with a reference type (such as the pointer to that record) but that makes this API a bit less convenient when you just want to store that result to a variable of that record type (would need an extra call or a deref after the last method call) Share this post Link to post
Marat1961 17 Posted October 28, 2020 (edited) unit Uri; interface uses System.SysUtils; type PxUri = ^TxUri; TxUri = record private FScheme: string; FPort: Cardinal; FHost: string; FUserName: string; FPassword: string; FPath: string; FQuery: string; FFragment: string; public function Init(const Scheme, Path: string; Port: Cardinal = 0): PxUri; function Host(const Value: string): PxUri; function UserName(const Value: string): PxUri; function Password(const Value: string): PxUri; function Path(const Value: string): PxUri; function Query(const Value: string): PxUri; function Fragment(const Value: string): PxUri; function ToString: string; end; implementation function TxUri.Init(const Scheme, Path: string; Port: Cardinal): PxUri; begin Result := @Self; Self := Default(TxUri); FScheme := Scheme; FPath := '/' + Path; FPort := Port; end; function TxUri.Host(const Value: string): PxUri; begin Result := @Self; FHost := Value; end; function TxUri.UserName(const Value: string): PxUri; begin Result := @Self; FUserName := Value; end; function TxUri.Password(const Value: string): PxUri; begin Result := @Self; FPassword := Value; end; function TxUri.Path(const Value: string): PxUri; begin Result := @Self; FPath := FPath + Format('/%s', [Value]); end; function TxUri.Query(const Value: string): PxUri; begin Result := @Self; if FQuery = '' then FQuery := Format('?%s', [Value]) else FQuery := FQuery + Format('&%s', [Value]); end; function TxUri.Fragment(const Value: string): PxUri; begin Result := @Self; FFragment := Format('#%s', [Value]); end; function TxUri.ToString: string; var sb: TStringBuilder; begin sb := TStringBuilder.Create; try sb.Append(FScheme); sb.Append(':'); if FHost <> '' then begin sb.Append('//'); if FUserName <> '' then sb.AppendFormat('%s:%s@', [FUserName, FPassword]); sb.Append(FHost); if FPort <> 0 then sb.AppendFormat(':%d', [FPort]); end; sb.Append(FPath); if FQuery <> '' then sb.Append(FQuery); if FFragment <> '' then sb.Append(FFragment); Result := sb.ToString; finally sb.Free; end; end; end. procedure TForm2.Button1Click(Sender: TObject); var uri: TxUri; begin uri.Init('http', 'Mammalia', 8080) .UserName('yourname') .Password('TopSecret') .Host('www.website.com') .Path('Theria') .Path('Carnivora') .Path('Felidae') .Path('Lynx_pardinus') .Query('showall') .Query('yearstart=1990') .Fragment('StartReadingHere'); Label1.Caption := uri.ToString; end; http://yourname:TopSecret@www.website.com:8080/Mammalia/Theria/Carnivora/Felidae/Lynx_pardinus?showall&yearstart=1990#StartReadingHere 005FD859 68901F0000 push $00001f90 005FD85E 8D45D8 lea eax,[ebp-$28] 005FD861 B92CD95F00 mov ecx,$005fd92c 005FD866 BA4CD95F00 mov edx,$005fd94c 005FD86B E8C8F8FFFF call TxUri.Init 005FD870 BA64D95F00 mov edx,$005fd964 005FD875 E856F9FFFF call TxUri.UserName 005FD87A BA84D95F00 mov edx,$005fd984 005FD87F E874F9FFFF call TxUri.Password 005FD884 BAA4D95F00 mov edx,$005fd9a4 005FD889 E81AF9FFFF call TxUri.Host 005FD88E BAD0D95F00 mov edx,$005fd9d0 005FD893 E888F9FFFF call TxUri.Path 005FD898 BAECD95F00 mov edx,$005fd9ec 005FD89D E87EF9FFFF call TxUri.Path 005FD8A2 BA0CDA5F00 mov edx,$005fda0c 005FD8A7 E874F9FFFF call TxUri.Path 005FD8AC BA28DA5F00 mov edx,$005fda28 005FD8B1 E86AF9FFFF call TxUri.Path 005FD8B6 BA50DA5F00 mov edx,$005fda50 005FD8BB E8ECF9FFFF call TxUri.Query 005FD8C0 BA6CDA5F00 mov edx,$005fda6c 005FD8C5 E8E2F9FFFF call TxUri.Query 005FD8CA BA98DA5F00 mov edx,$005fda98 005FD8CF E8B8FAFFFF call TxUri.Fragment TestUri.pas.42: Label1.Caption := uri.ToString; 005FD8D4 8D55D4 lea edx,[ebp-$2c] 005FD8D7 8D45D8 lea eax,[ebp-$28] 005FD8DA E835FBFFFF call TxUri.ToString 005FD8DF 8B55D4 mov edx,[ebp-$2c] 005FD8E2 8B45FC mov eax,[ebp-$04] 005FD8E5 8B80D4030000 mov eax,[eax+$000003d4] 005FD8EB E8F446F4FF call TControl.SetText Edited October 28, 2020 by Marat1961 3 Share this post Link to post
Fr0sT.Brutal 900 Posted October 28, 2020 14 hours ago, Stefan Glienke said: Fluent API with records containing managed fields (such as strings) unfortunately produces terrible code because the compiler produces an implicit variable for each method call It will produce implicit finally section for strings anyway so not a big problem I suppose. Share this post Link to post
mvanrijnen 123 Posted October 28, 2020 14 hours ago, Stefan Glienke said: Fluent API with records containing managed fields (such as strings) unfortunately produces terrible code because the compiler produces an implicit variable for each method call. Ok, did not know that. So have to be carefull where and how to use this. Would not be wise to use it in highperf code sections, but There where you set up connection strings, sql statements, urls etc, why not use it ? Share this post Link to post
mvanrijnen 123 Posted October 28, 2020 (edited) 5 hours ago, Marat1961 said: .. http://yourname:TopSecret@www.website.com:8080/Mammalia/Theria/Carnivora/Felidae/Lynx_pardinus?showall&yearstart=1990#StartReadingHere Good idea with the pointer, you do have to encode somevalues in the stringbuilder (escape special chars etc) maybe with an TInterfaced (no need to free) THSURI2 = class(TInterfacedObject) private FScheme: THSURIScheme; FFragment: string; FQuery: string; FPort: Integer; FPassword: string; FHost: string; FUserName: string; FPath : string; function GetAsString: string; constructor Scheme(const AScheme : THSURIScheme); public constructor HTTP; constructor HTTPS; .... TMP2 := THSURI2.HTTPS .UserName('yourname') .Password('andpassword') .Host('www.website.com') .Port(1234) .Path('chapter') .Path('subchapter') .Path('subsubchaper') .Parameter('showall') .Parameter('yearstart', '1990') .Fragment('startreadinghere'); test := TMP2.AsString; Edited October 28, 2020 by mvanrijnen Share this post Link to post