Move AST to AST.Document
This commit is contained in:
parent
78ee76f9d5
commit
fdf5914626
@ -11,9 +11,11 @@ and this project adheres to
|
|||||||
- AST for the GraphQL schema.
|
- AST for the GraphQL schema.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Rename `AST.Definition` into `AST.ExecutableDefinition`.
|
- Rename `AST.Definition` into `AST.Document.ExecutableDefinition`.
|
||||||
`AST.TypeSystemDefinition` and `AST.TypeSystemExtension` can also be
|
`AST.Document.TypeSystemDefinition` and `AST.Document.TypeSystemExtension`
|
||||||
definitions.
|
can also be definitions.
|
||||||
|
- Move all AST data to `AST.Document` and reexport them.
|
||||||
|
- Rename `AST.OperationSelectionSet` to `AST.Document.SelectionSet`.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- `AST.Field`, `AST.InlineFragment` and `AST.FragmentSpread`.
|
- `AST.Field`, `AST.InlineFragment` and `AST.FragmentSpread`.
|
||||||
|
@ -1,157 +1,6 @@
|
|||||||
-- | Target AST for Parser.
|
-- | Target AST for Parser.
|
||||||
module Language.GraphQL.AST
|
module Language.GraphQL.AST
|
||||||
( Alias
|
( module Language.GraphQL.AST.Document
|
||||||
, Argument(..)
|
|
||||||
, ExecutableDefinition(..)
|
|
||||||
, Directive(..)
|
|
||||||
, FragmentDefinition(..)
|
|
||||||
, Name
|
|
||||||
, NonNullType(..)
|
|
||||||
, ObjectField(..)
|
|
||||||
, OperationDefinition(..)
|
|
||||||
, OperationType(..)
|
|
||||||
, Selection(..)
|
|
||||||
, SelectionSet
|
|
||||||
, SelectionSetOpt
|
|
||||||
, Type(..)
|
|
||||||
, TypeCondition
|
|
||||||
, Value(..)
|
|
||||||
, VariableDefinition(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Int (Int32)
|
import Language.GraphQL.AST.Document
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
|
||||||
import Data.Text (Text)
|
|
||||||
|
|
||||||
-- | Name
|
|
||||||
type Name = Text
|
|
||||||
|
|
||||||
-- | Directive.
|
|
||||||
data Directive = Directive Name [Argument] deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- * Operations
|
|
||||||
|
|
||||||
-- | Top-level definition of a document, either an operation or a fragment.
|
|
||||||
data ExecutableDefinition
|
|
||||||
= DefinitionOperation OperationDefinition
|
|
||||||
| DefinitionFragment FragmentDefinition
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Operation definition.
|
|
||||||
data OperationDefinition
|
|
||||||
= OperationSelectionSet SelectionSet
|
|
||||||
| OperationDefinition OperationType
|
|
||||||
(Maybe Name)
|
|
||||||
[VariableDefinition]
|
|
||||||
[Directive]
|
|
||||||
SelectionSet
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | GraphQL has 3 operation types: queries, mutations and subscribtions.
|
|
||||||
--
|
|
||||||
-- Currently only queries and mutations are supported.
|
|
||||||
data OperationType = Query | Mutation deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- * Selections
|
|
||||||
|
|
||||||
-- | "Top-level" selection, selection on an operation or fragment.
|
|
||||||
type SelectionSet = NonEmpty Selection
|
|
||||||
|
|
||||||
-- | Field selection.
|
|
||||||
type SelectionSetOpt = [Selection]
|
|
||||||
|
|
||||||
-- | Single GraphQL field.
|
|
||||||
--
|
|
||||||
-- The only required property of a field is its name. Optionally it can also
|
|
||||||
-- have an alias, arguments or a list of subfields.
|
|
||||||
--
|
|
||||||
-- Given the following query:
|
|
||||||
--
|
|
||||||
-- @
|
|
||||||
-- {
|
|
||||||
-- zuck: user(id: 4) {
|
|
||||||
-- id
|
|
||||||
-- name
|
|
||||||
-- }
|
|
||||||
-- }
|
|
||||||
-- @
|
|
||||||
--
|
|
||||||
-- * "user", "id" and "name" are field names.
|
|
||||||
-- * "user" has two subfields, "id" and "name".
|
|
||||||
-- * "zuck" is an alias for "user". "id" and "name" have no aliases.
|
|
||||||
-- * "id: 4" is an argument for "user". "id" and "name" don't have any
|
|
||||||
-- arguments.
|
|
||||||
data Selection
|
|
||||||
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
|
|
||||||
| FragmentSpread Name [Directive]
|
|
||||||
| InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Alternative field name.
|
|
||||||
--
|
|
||||||
-- @
|
|
||||||
-- {
|
|
||||||
-- smallPic: profilePic(size: 64)
|
|
||||||
-- bigPic: profilePic(size: 1024)
|
|
||||||
-- }
|
|
||||||
-- @
|
|
||||||
--
|
|
||||||
-- Here "smallPic" and "bigPic" are aliases for the same field, "profilePic",
|
|
||||||
-- used to distinquish between profile pictures with different arguments
|
|
||||||
-- (sizes).
|
|
||||||
type Alias = Name
|
|
||||||
|
|
||||||
-- | Single argument.
|
|
||||||
--
|
|
||||||
-- @
|
|
||||||
-- {
|
|
||||||
-- user(id: 4) {
|
|
||||||
-- name
|
|
||||||
-- }
|
|
||||||
-- }
|
|
||||||
-- @
|
|
||||||
--
|
|
||||||
-- Here "id" is an argument for the field "user" and its value is 4.
|
|
||||||
data Argument = Argument Name Value deriving (Eq,Show)
|
|
||||||
|
|
||||||
-- | Fragment definition.
|
|
||||||
data FragmentDefinition
|
|
||||||
= FragmentDefinition Name TypeCondition [Directive] SelectionSet
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- * Inputs
|
|
||||||
|
|
||||||
-- | Input value.
|
|
||||||
data Value = Variable Name
|
|
||||||
| Int Int32
|
|
||||||
| Float Double
|
|
||||||
| String Text
|
|
||||||
| Boolean Bool
|
|
||||||
| Null
|
|
||||||
| Enum Name
|
|
||||||
| List [Value]
|
|
||||||
| Object [ObjectField]
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Key-value pair.
|
|
||||||
--
|
|
||||||
-- A list of 'ObjectField's represents a GraphQL object type.
|
|
||||||
data ObjectField = ObjectField Name Value deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Variable definition.
|
|
||||||
data VariableDefinition = VariableDefinition Name Type (Maybe Value)
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Type condition.
|
|
||||||
type TypeCondition = Name
|
|
||||||
|
|
||||||
-- | Type representation.
|
|
||||||
data Type = TypeNamed Name
|
|
||||||
| TypeList Type
|
|
||||||
| TypeNonNull NonNullType
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | Helper type to represent Non-Null types and lists of such types.
|
|
||||||
data NonNullType = NonNullTypeNamed Name
|
|
||||||
| NonNullTypeList Type
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
@ -1,34 +1,46 @@
|
|||||||
-- | This module defines an abstract syntax tree for the @GraphQL@ language. It
|
-- | This module defines an abstract syntax tree for the @GraphQL@ language. It
|
||||||
-- follows closely the structure given in the specification. Please refer to
|
-- follows closely the structure given in the specification. Please refer to
|
||||||
-- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
|
-- <https://facebook.github.io/graphql/ Facebook's GraphQL Specification>.
|
||||||
-- for more information.
|
-- for more information.
|
||||||
module Language.GraphQL.AST.Document
|
module Language.GraphQL.AST.Document
|
||||||
( Definition(..)
|
( Alias
|
||||||
|
, Argument(..)
|
||||||
|
, Definition(ExecutableDefinition)
|
||||||
|
, Directive(..)
|
||||||
, Document
|
, Document
|
||||||
, ExecutableDefinition(..)
|
, ExecutableDefinition(..)
|
||||||
|
, FragmentDefinition(..)
|
||||||
|
, Name
|
||||||
|
, NonNullType(..)
|
||||||
|
, ObjectField(..)
|
||||||
|
, OperationDefinition(..)
|
||||||
|
, OperationType(..)
|
||||||
|
, Selection(..)
|
||||||
|
, SelectionSet
|
||||||
|
, SelectionSetOpt
|
||||||
|
, Type(..)
|
||||||
|
, TypeCondition
|
||||||
|
, Value(..)
|
||||||
|
, VariableDefinition(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Data.Int (Int32)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Language.GraphQL.AST
|
|
||||||
( ExecutableDefinition(..)
|
|
||||||
, Directive
|
|
||||||
, Name
|
|
||||||
, OperationType
|
|
||||||
, Type
|
|
||||||
, Value
|
|
||||||
)
|
|
||||||
import Language.GraphQL.AST.DirectiveLocation
|
import Language.GraphQL.AST.DirectiveLocation
|
||||||
|
|
||||||
-- * Language
|
-- * Language
|
||||||
|
|
||||||
|
-- ** Source Text
|
||||||
|
|
||||||
|
-- | Name.
|
||||||
|
type Name = Text
|
||||||
|
|
||||||
-- ** Document
|
-- ** Document
|
||||||
|
|
||||||
-- | GraphQL document.
|
-- | GraphQL document.
|
||||||
type Document = NonEmpty Definition
|
type Document = NonEmpty Definition
|
||||||
|
|
||||||
type NamedType = Name
|
|
||||||
|
|
||||||
-- | All kinds of definitions that can occur in a GraphQL document.
|
-- | All kinds of definitions that can occur in a GraphQL document.
|
||||||
data Definition
|
data Definition
|
||||||
= ExecutableDefinition ExecutableDefinition
|
= ExecutableDefinition ExecutableDefinition
|
||||||
@ -36,12 +48,190 @@ data Definition
|
|||||||
| TypeSystemExtension TypeSystemExtension
|
| TypeSystemExtension TypeSystemExtension
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | Top-level definition of a document, either an operation or a fragment.
|
||||||
|
data ExecutableDefinition
|
||||||
|
= DefinitionOperation OperationDefinition
|
||||||
|
| DefinitionFragment FragmentDefinition
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Operations
|
||||||
|
|
||||||
|
-- | Operation definition.
|
||||||
|
data OperationDefinition
|
||||||
|
= SelectionSet SelectionSet
|
||||||
|
| OperationDefinition
|
||||||
|
OperationType
|
||||||
|
(Maybe Name)
|
||||||
|
[VariableDefinition]
|
||||||
|
[Directive]
|
||||||
|
SelectionSet
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | GraphQL has 3 operation types:
|
||||||
|
--
|
||||||
|
-- * query - a read-only fetch.
|
||||||
|
-- * mutation - a write operation followed by a fetch.
|
||||||
|
-- * subscription - a long-lived request that fetches data in response to
|
||||||
|
-- source events.
|
||||||
|
--
|
||||||
|
-- Currently only queries and mutations are supported.
|
||||||
|
data OperationType = Query | Mutation deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Selection Sets
|
||||||
|
|
||||||
|
-- | "Top-level" selection, selection on an operation or fragment.
|
||||||
|
type SelectionSet = NonEmpty Selection
|
||||||
|
|
||||||
|
-- | Field selection.
|
||||||
|
type SelectionSetOpt = [Selection]
|
||||||
|
|
||||||
|
-- | Selection is a single entry in a selection set. It can be a single field,
|
||||||
|
-- fragment spread or inline fragment.
|
||||||
|
--
|
||||||
|
-- The only required property of a field is its name. Optionally it can also
|
||||||
|
-- have an alias, arguments, directives and a list of subfields.
|
||||||
|
--
|
||||||
|
-- In the following query "user" is a field with two subfields, "id" and "name":
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- {
|
||||||
|
-- user {
|
||||||
|
-- id
|
||||||
|
-- name
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- A fragment spread refers to a fragment defined outside the operation and is
|
||||||
|
-- expanded at the execution time.
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- {
|
||||||
|
-- user {
|
||||||
|
-- ...userFragment
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- fragment userFragment on UserType {
|
||||||
|
-- id
|
||||||
|
-- name
|
||||||
|
-- }
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- Inline fragments are similar but they don't have any name and the type
|
||||||
|
-- condition ("on UserType") is optional.
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- {
|
||||||
|
-- user {
|
||||||
|
-- ... on UserType {
|
||||||
|
-- id
|
||||||
|
-- name
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- @
|
||||||
|
data Selection
|
||||||
|
= Field (Maybe Alias) Name [Argument] [Directive] SelectionSetOpt
|
||||||
|
| FragmentSpread Name [Directive]
|
||||||
|
| InlineFragment (Maybe TypeCondition) [Directive] SelectionSet
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Arguments
|
||||||
|
|
||||||
|
-- | Single argument.
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- {
|
||||||
|
-- user(id: 4) {
|
||||||
|
-- name
|
||||||
|
-- }
|
||||||
|
-- }
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- Here "id" is an argument for the field "user" and its value is 4.
|
||||||
|
data Argument = Argument Name Value deriving (Eq,Show)
|
||||||
|
|
||||||
|
-- ** Field Alias
|
||||||
|
|
||||||
|
-- | Alternative field name.
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- {
|
||||||
|
-- smallPic: profilePic(size: 64)
|
||||||
|
-- bigPic: profilePic(size: 1024)
|
||||||
|
-- }
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- Here "smallPic" and "bigPic" are aliases for the same field, "profilePic",
|
||||||
|
-- used to distinquish between profile pictures with different arguments
|
||||||
|
-- (sizes).
|
||||||
|
type Alias = Name
|
||||||
|
|
||||||
|
-- ** Fragments
|
||||||
|
|
||||||
|
-- | Fragment definition.
|
||||||
|
data FragmentDefinition
|
||||||
|
= FragmentDefinition Name TypeCondition [Directive] SelectionSet
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | Type condition.
|
||||||
|
type TypeCondition = Name
|
||||||
|
|
||||||
|
-- ** Input Values
|
||||||
|
|
||||||
|
-- | Input value.
|
||||||
|
data Value
|
||||||
|
= Variable Name
|
||||||
|
| Int Int32
|
||||||
|
| Float Double
|
||||||
|
| String Text
|
||||||
|
| Boolean Bool
|
||||||
|
| Null
|
||||||
|
| Enum Name
|
||||||
|
| List [Value]
|
||||||
|
| Object [ObjectField]
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | Key-value pair.
|
||||||
|
--
|
||||||
|
-- A list of 'ObjectField's represents a GraphQL object type.
|
||||||
|
data ObjectField = ObjectField Name Value deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Variables
|
||||||
|
|
||||||
|
-- | Variable definition.
|
||||||
|
data VariableDefinition = VariableDefinition Name Type (Maybe Value)
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Type References
|
||||||
|
|
||||||
|
-- | Type representation.
|
||||||
|
data Type
|
||||||
|
= TypeNamed Name
|
||||||
|
| TypeList Type
|
||||||
|
| TypeNonNull NonNullType
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
type NamedType = Name
|
||||||
|
|
||||||
|
-- | Helper type to represent Non-Null types and lists of such types.
|
||||||
|
data NonNullType
|
||||||
|
= NonNullTypeNamed Name
|
||||||
|
| NonNullTypeList Type
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- ** Directives
|
||||||
|
|
||||||
|
-- | Directive.
|
||||||
|
data Directive = Directive Name [Argument] deriving (Eq, Show)
|
||||||
|
|
||||||
-- * Type System
|
-- * Type System
|
||||||
|
|
||||||
data TypeSystemDefinition
|
data TypeSystemDefinition
|
||||||
= SchemaDefinition [Directive] RootOperationTypeDefinitions
|
= SchemaDefinition [Directive] RootOperationTypeDefinitions
|
||||||
| TypeDefinition TypeDefinition
|
| TypeDefinition TypeDefinition
|
||||||
| DirectiveDefinition Description Name ArgumentsDefinition DirectiveLocation
|
| DirectiveDefinition
|
||||||
|
Description Name ArgumentsDefinition DirectiveLocation
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
-- ** Type System Extensions
|
-- ** Type System Extensions
|
||||||
@ -109,7 +299,8 @@ newtype ImplementsInterfaces = ImplementsInterfaces (NonEmpty NamedType)
|
|||||||
newtype ImplementsInterfacesOpt = ImplementsInterfacesOpt [NamedType]
|
newtype ImplementsInterfacesOpt = ImplementsInterfacesOpt [NamedType]
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data FieldDefinition = FieldDefinition Description Name ArgumentsDefinition Type
|
data FieldDefinition
|
||||||
|
= FieldDefinition Description Name ArgumentsDefinition Type
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
newtype ArgumentsDefinition = ArgumentsDefinition [InputValueDefinition]
|
newtype ArgumentsDefinition = ArgumentsDefinition [InputValueDefinition]
|
||||||
|
@ -66,7 +66,7 @@ definition formatter x
|
|||||||
= fragmentDefinition formatter fragment
|
= fragmentDefinition formatter fragment
|
||||||
|
|
||||||
operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
|
operationDefinition :: Formatter -> Full.OperationDefinition -> Lazy.Text
|
||||||
operationDefinition formatter (Full.OperationSelectionSet sels)
|
operationDefinition formatter (Full.SelectionSet sels)
|
||||||
= selectionSet formatter sels
|
= selectionSet formatter sels
|
||||||
operationDefinition formatter (Full.OperationDefinition Full.Query name vars dirs sels)
|
operationDefinition formatter (Full.OperationDefinition Full.Query name vars dirs sels)
|
||||||
= "query " <> node formatter name vars dirs sels
|
= "query " <> node formatter name vars dirs sels
|
||||||
|
@ -25,13 +25,16 @@ definition = DefinitionOperation <$> operationDefinition
|
|||||||
<?> "definition error!"
|
<?> "definition error!"
|
||||||
|
|
||||||
operationDefinition :: Parser OperationDefinition
|
operationDefinition :: Parser OperationDefinition
|
||||||
operationDefinition = OperationSelectionSet <$> selectionSet
|
operationDefinition = SelectionSet <$> selectionSet
|
||||||
<|> OperationDefinition <$> operationType
|
<|> operationDefinition'
|
||||||
<*> optional name
|
<?> "operationDefinition error"
|
||||||
<*> opt variableDefinitions
|
where
|
||||||
<*> opt directives
|
operationDefinition'
|
||||||
<*> selectionSet
|
= OperationDefinition <$> operationType
|
||||||
<?> "operationDefinition error"
|
<*> optional name
|
||||||
|
<*> opt variableDefinitions
|
||||||
|
<*> opt directives
|
||||||
|
<*> selectionSet
|
||||||
|
|
||||||
operationType :: Parser OperationType
|
operationType :: Parser OperationType
|
||||||
operationType = Query <$ symbol "query"
|
operationType = Query <$ symbol "query"
|
||||||
|
@ -58,13 +58,13 @@ operations operations' = do
|
|||||||
lift . lift $ NonEmpty.nonEmpty coreOperations
|
lift . lift $ NonEmpty.nonEmpty coreOperations
|
||||||
|
|
||||||
operation :: Full.OperationDefinition -> TransformT Core.Operation
|
operation :: Full.OperationDefinition -> TransformT Core.Operation
|
||||||
operation (Full.OperationSelectionSet sels) =
|
operation (Full.SelectionSet sels)
|
||||||
operation $ Full.OperationDefinition Full.Query mempty mempty mempty sels
|
= operation $ Full.OperationDefinition Full.Query mempty mempty mempty sels
|
||||||
-- TODO: Validate Variable definitions with substituter
|
-- TODO: Validate Variable definitions with substituter
|
||||||
operation (Full.OperationDefinition Full.Query name _vars _dirs sels) =
|
operation (Full.OperationDefinition Full.Query name _vars _dirs sels)
|
||||||
Core.Query name <$> appendSelection sels
|
= Core.Query name <$> appendSelection sels
|
||||||
operation (Full.OperationDefinition Full.Mutation name _vars _dirs sels) =
|
operation (Full.OperationDefinition Full.Mutation name _vars _dirs sels)
|
||||||
Core.Mutation name <$> appendSelection sels
|
= Core.Mutation name <$> appendSelection sels
|
||||||
|
|
||||||
-- * Selection
|
-- * Selection
|
||||||
|
|
||||||
|
@ -35,8 +35,7 @@ spec = do
|
|||||||
it "indents block strings in arguments" $
|
it "indents block strings in arguments" $
|
||||||
let arguments = [Argument "message" (String "line1\nline2")]
|
let arguments = [Argument "message" (String "line1\nline2")]
|
||||||
field = Field Nothing "field" arguments [] []
|
field = Field Nothing "field" arguments [] []
|
||||||
set = OperationSelectionSet $ pure field
|
operation = DefinitionOperation $ SelectionSet $ pure field
|
||||||
operation = DefinitionOperation set
|
|
||||||
in definition pretty operation `shouldBe` [r|{
|
in definition pretty operation `shouldBe` [r|{
|
||||||
field(message: """
|
field(message: """
|
||||||
line1
|
line1
|
||||||
|
Loading…
Reference in New Issue
Block a user