Jump to content

Recommended Posts

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 by Attila Kovacs

Share this post


Link to post

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

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

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

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


 

  // 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 by Marat1961

Share this post


Link to post
Guest

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

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 by Marat1961

Share this post


Link to post

@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

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

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
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

@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
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 by Marat1961

Share this post


Link to post

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 by mvanrijnen
  • Like 1

Share this post


Link to post

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. :classic_sad:

  • Like 1
  • Sad 1

Share this post


Link to post
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. :classic_sad:

Undoubtedly a string builder must be used inside,
but what a convenient and intuitive interface!

Share this post


Link to post
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
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 by Marat1961
  • Like 3

Share this post


Link to post
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
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. :classic_sad:

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
5 hours ago, Marat1961 said:

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 by mvanrijnen

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×